Skip to main content

bedrock_world/
world.rs

1//! High-level lazy world access built on top of the storage layer.
2//!
3//! The methods in this module are intentionally split into blocking and async
4//! forms. Blocking methods are the canonical implementation and are appropriate
5//! for CLI tools, background worker threads, and tests. Async methods are thin
6//! wrappers that offload the same work with `tokio::task::spawn_blocking`.
7
8use crate::chunk::{
9    ActorDigestKey, ActorUid, BedrockDbKey, BlockPos, BlockState, Chunk, ChunkKey, ChunkPos,
10    ChunkRecord, ChunkRecordTag, ChunkVersion, GlobalRecordKind, LegacyBiomeSample, LegacyTerrain,
11    MapRecordId, SubChunk, SubChunkDecodeMode, parse_subchunk_with_mode,
12};
13use crate::error::{BedrockWorldError, Result};
14use crate::level_dat::{LevelDatDocument, read_level_dat_document, write_level_dat_document};
15use crate::nbt::{NbtTag, parse_consecutive_root_nbt, parse_root_nbt, serialize_root_nbt};
16use crate::parsed::{
17    ActorRecord, ActorSource, Biome2d, Biome3d, BlockEntityRecord, HeightMap2d, ItemStack,
18    ParsedBiomeData, ParsedBiomeStorage, ParsedBlockEntity, ParsedChunkData, ParsedDbEntry,
19    ParsedDbValue, ParsedEntity, ParsedGlobalData, ParsedHardcodedSpawnArea, ParsedMapData,
20    ParsedVillageData, ParsedWorld, WorldParseOptions, WorldParseReport, collect_item_stacks,
21    encode_actor_digest_ids, encode_consecutive_roots, encode_global_record,
22    encode_hardcoded_spawn_area_records, encode_map_record, parse_actor_digest_ids,
23    parse_block_entities_from_value, parse_chunk_records, parse_chunk_records_with_options,
24    parse_data3d, parse_entities_from_value, parse_global_record, parse_global_storage_entries,
25    parse_hardcoded_spawn_area_records, parse_legacy_data2d, parse_map_record, parse_world_storage,
26};
27use crate::player::{PlayerData, PlayerId};
28use crate::storage::backend::BedrockLevelDbStorage;
29use crate::storage::{
30    PocketChunksDatStorage, StorageBatch, StorageCachePolicy, StorageCancelFlag, StorageOp,
31    StorageProgressSink, StorageReadOptions, StorageScanMode, StorageThreadingOptions,
32    StorageVisitorControl, WorldStorage,
33};
34pub use crate::surface::{TerrainSurfaceRole, terrain_surface_overlay_alpha, terrain_surface_role};
35use crate::surface::{is_air_block_name, is_water_block_name};
36use bytes::Bytes;
37use rayon::{ThreadPoolBuilder, prelude::*};
38use std::path::{Path, PathBuf};
39use std::sync::Arc;
40use std::time::Instant;
41use std::{
42    collections::{BTreeMap, BTreeSet},
43    sync::{
44        Mutex,
45        atomic::{AtomicBool, Ordering},
46        mpsc,
47    },
48};
49
50/// Options used when opening or constructing a [`BedrockWorld`].
51#[derive(Debug, Clone)]
52pub struct OpenOptions {
53    /// Reject mutating operations when set.
54    pub read_only: bool,
55    /// Preferred world storage format. [`WorldFormatHint::Auto`] detects the
56    /// backend from `db/CURRENT` and old `chunks.dat` files.
57    pub format: WorldFormatHint,
58}
59
60impl Default for OpenOptions {
61    fn default() -> Self {
62        Self {
63            read_only: true,
64            format: WorldFormatHint::Auto,
65        }
66    }
67}
68
69#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
70/// Preferred storage format selection used when opening a world.
71pub enum WorldFormatHint {
72    #[default]
73    /// Automatically choose the appropriate mode.
74    Auto,
75    /// Modern Bedrock `LevelDB` world.
76    LevelDb,
77    /// Pre-`LevelDB` Pocket Edition `chunks.dat` world.
78    PocketChunksDat,
79}
80
81#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
82/// Detected world storage format.
83pub enum WorldFormat {
84    #[default]
85    /// Modern Bedrock `LevelDB` world.
86    LevelDb,
87    /// Old `LevelDB` world using `LegacyTerrain` records.
88    LevelDbLegacyTerrain,
89    /// Pre-`LevelDB` Pocket Edition `chunks.dat` world.
90    PocketChunksDat,
91}
92
93/// Lazy handle to a Minecraft Bedrock world folder.
94///
95/// A handle stores the world path and a storage backend. It does not scan or
96/// parse the database until a query method is called.
97pub struct BedrockWorld<S = Arc<dyn WorldStorage>> {
98    path: PathBuf,
99    options: OpenOptions,
100    storage: S,
101    format: WorldFormat,
102}
103
104/// Storage handle accepted by generic [`BedrockWorld`] methods.
105pub trait WorldStorageHandle: Clone + Send + Sync + 'static {
106    /// Returns the raw storage backend behind this handle.
107    fn storage(&self) -> &dyn WorldStorage;
108}
109
110impl<T> WorldStorageHandle for T
111where
112    T: WorldStorage + Clone + Send + Sync + 'static,
113{
114    fn storage(&self) -> &dyn WorldStorage {
115        self
116    }
117}
118
119impl<T> WorldStorageHandle for Arc<T>
120where
121    T: WorldStorage + 'static,
122{
123    fn storage(&self) -> &dyn WorldStorage {
124        self.as_ref()
125    }
126}
127
128impl WorldStorageHandle for Arc<dyn WorldStorage> {
129    fn storage(&self) -> &dyn WorldStorage {
130        self.as_ref()
131    }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135/// Options for surface-column lookup.
136pub struct SurfaceColumnOptions {
137    /// Whether air blocks are skipped when finding a surface column.
138    pub skip_air: bool,
139    /// Whether water is treated as transparent context over terrain.
140    pub transparent_water: bool,
141}
142
143impl Default for SurfaceColumnOptions {
144    fn default() -> Self {
145        Self {
146            skip_air: true,
147            transparent_water: true,
148        }
149    }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
153/// Legacy surface-column query result.
154pub struct SurfaceColumn {
155    /// World Y coordinate selected as the visible surface.
156    pub y: i32,
157    /// Block name selected for this result.
158    pub block_name: String,
159    /// Biome id associated with the sampled column.
160    pub biome_id: Option<u32>,
161    /// Number of water blocks above the underwater support block.
162    pub water_depth: u8,
163    /// Block name below water, when found.
164    pub under_water_block_name: Option<String>,
165    /// Whether the value came from a fallback path.
166    pub is_fallback: bool,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170/// Controls how much subchunk data exact surface loading reads.
171pub enum ExactSurfaceSubchunkPolicy {
172    /// Load the full subchunk range required by the request.
173    Full,
174    /// Use height hints first and reload when verification requires it.
175    HintThenVerify,
176}
177
178#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
179/// Bounded pipeline settings for world scans and render loads.
180pub struct WorldPipelineOptions {
181    /// Maximum queued work items; zero selects an automatic default.
182    pub queue_depth: usize,
183    /// Chunk batch size; zero selects an automatic default.
184    pub chunk_batch_size: usize,
185    /// Subchunk decode worker count; zero selects an automatic default.
186    pub subchunk_decode_workers: usize,
187    /// Progress callback interval; zero selects an automatic default.
188    pub progress_interval: usize,
189}
190
191impl WorldPipelineOptions {
192    #[must_use]
193    /// Resolves the effective bounded queue depth.
194    pub fn resolve_queue_depth(self, workers: usize, work_items: usize) -> usize {
195        self.queue_depth
196            .max(if self.queue_depth == 0 {
197                workers
198                    .max(1)
199                    .saturating_mul(2)
200                    .max(work_items.clamp(1, 256))
201            } else {
202                1
203            })
204            .max(1)
205    }
206
207    #[must_use]
208    /// Resolves the effective progress callback interval.
209    pub fn resolve_progress_interval(self) -> usize {
210        self.progress_interval
211            .max(if self.progress_interval == 0 { 256 } else { 1 })
212    }
213}
214
215#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
216/// Ordering policy for render chunk loading.
217pub enum ChunkLoadPriority {
218    #[default]
219    /// Process chunks in row-major order.
220    RowMajor,
221    /// Prioritize chunks by distance from a center chunk.
222    DistanceFrom {
223        /// Center chunk X coordinate used for distance sorting.
224        chunk_x: i32,
225        /// Center chunk Z coordinate used for distance sorting.
226        chunk_z: i32,
227    },
228}
229
230#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
231/// Biome loading policy for exact surface requests.
232pub enum ExactSurfaceBiomeLoad {
233    /// No optional data is requested or available.
234    None,
235    #[default]
236    /// Load biome data needed for top-column sampling.
237    TopColumns,
238    /// Load all matching biome data.
239    All,
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243/// One independently requested subchunk representation.
244pub enum SubchunkDataRequirement {
245    /// Compute exact visible terrain columns without materializing 3D indices.
246    SurfaceColumns(ExactSurfaceSubchunkPolicy),
247    /// Decode one fixed world Y layer with 3D random access.
248    Layer(i32),
249    /// Decode one cave slice with 3D random access.
250    CaveSlice(i32),
251    /// Decode every subchunk in the chunk with full 3D random access.
252    Full3dIndices,
253}
254
255#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
256/// Biome payload requested with map data.
257pub enum BiomeDataRequirement {
258    /// Do not load optional biome records.
259    #[default]
260    None,
261    /// Load biome data needed by surface-column sampling.
262    SurfaceColumns,
263    /// Load biome data for one world Y layer.
264    Layer(i32),
265    /// Retain every matching biome storage.
266    All,
267}
268
269#[derive(Debug, Clone, Default, PartialEq, Eq)]
270/// Composable map-data contract used to plan storage reads and subchunk decoding.
271///
272/// Add only the representations a consumer needs. The loader unions all requested
273/// subchunk records and chooses the least expensive decoder that satisfies them.
274pub struct ChunkDataRequest {
275    /// Independent subchunk representations to load.
276    pub subchunks: Vec<SubchunkDataRequirement>,
277    /// Whether raw height-map data is required.
278    pub height_map: bool,
279    /// Optional biome payload requirement.
280    pub biome: BiomeDataRequirement,
281    /// Whether block-entity NBT is required.
282    pub block_entities: bool,
283}
284
285impl ChunkDataRequest {
286    #[must_use]
287    /// Starts an empty request.
288    pub const fn new() -> Self {
289        Self {
290            subchunks: Vec::new(),
291            height_map: false,
292            biome: BiomeDataRequirement::None,
293            block_entities: false,
294        }
295    }
296
297    #[must_use]
298    /// Requests exact terrain columns with the selected subchunk probing policy.
299    pub fn surface_columns(mut self, policy: ExactSurfaceSubchunkPolicy) -> Self {
300        self.push_subchunk_requirement(SubchunkDataRequirement::SurfaceColumns(policy));
301        self
302    }
303
304    #[must_use]
305    /// Requests a fixed Y layer.
306    pub fn layer(mut self, y: i32) -> Self {
307        self.push_subchunk_requirement(SubchunkDataRequirement::Layer(y));
308        self
309    }
310
311    #[must_use]
312    /// Requests a cave slice at one world Y coordinate.
313    pub fn cave_slice(mut self, y: i32) -> Self {
314        self.push_subchunk_requirement(SubchunkDataRequirement::CaveSlice(y));
315        self
316    }
317
318    #[must_use]
319    /// Requests full 3D random-access indices for every subchunk in a chunk.
320    pub fn full_3d_indices(mut self) -> Self {
321        self.push_subchunk_requirement(SubchunkDataRequirement::Full3dIndices);
322        self
323    }
324
325    #[must_use]
326    /// Requests raw height-map data.
327    pub const fn height_map(mut self) -> Self {
328        self.height_map = true;
329        self
330    }
331
332    #[must_use]
333    /// Sets the optional biome payload requirement.
334    pub const fn biome(mut self, biome: BiomeDataRequirement) -> Self {
335        self.biome = biome;
336        self
337    }
338
339    #[must_use]
340    /// Requests block-entity NBT records.
341    pub const fn block_entities(mut self) -> Self {
342        self.block_entities = true;
343        self
344    }
345
346    fn push_subchunk_requirement(&mut self, requirement: SubchunkDataRequirement) {
347        if !self.subchunks.contains(&requirement) {
348            self.subchunks.push(requirement);
349        }
350    }
351}
352
353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354/// Source payload used for a terrain column sample.
355pub enum TerrainSampleSource {
356    /// Data sourced from decoded subchunks.
357    Subchunk,
358    /// Old `LevelDB`-era terrain record.
359    LegacyTerrain,
360    /// Data sourced from legacy terrain as a fallback.
361    LegacyFallback,
362}
363
364#[derive(Debug, Clone, Copy, PartialEq, Eq)]
365/// Biome value associated with a terrain column.
366pub enum TerrainColumnBiome {
367    /// Numeric biome id value.
368    Id(u32),
369    /// Legacy biome sample value.
370    Legacy(LegacyBiomeSample),
371}
372
373#[derive(Debug, Clone, PartialEq)]
374/// Thin overlay block above a sampled surface.
375pub struct TerrainColumnOverlay {
376    /// World Y coordinate of the overlay block.
377    pub y: i16,
378    /// Block state selected as the overlay.
379    pub block_state: BlockState,
380    /// Storage or terrain source that produced this value.
381    pub source: TerrainSampleSource,
382}
383
384#[derive(Debug, Clone, PartialEq)]
385/// Water context for a sampled surface column.
386pub struct TerrainColumnWater {
387    /// Y coordinate of the visible surface block.
388    pub surface_y: i16,
389    /// Water block state at the visible surface.
390    pub block_state: BlockState,
391    /// Depth in blocks.
392    pub depth: u8,
393    /// Y coordinate of the first underwater support block, when found.
394    pub underwater_y: Option<i16>,
395    /// Block state below water, when found.
396    pub underwater_block_state: Option<BlockState>,
397    /// Storage or terrain source that produced this value.
398    pub source: TerrainSampleSource,
399}
400
401#[derive(Debug, Clone, PartialEq)]
402/// Canonical terrain surface sample for one local X/Z column.
403pub struct TerrainColumnSample {
404    /// Y coordinate of the visible surface block.
405    pub surface_y: i16,
406    /// Block state selected as the visible surface.
407    pub surface_block_state: BlockState,
408    /// Y coordinate of the supporting relief block.
409    pub relief_y: i16,
410    /// Block state selected as relief/support.
411    pub relief_block_state: BlockState,
412    /// Optional thin overlay block above the primary surface.
413    pub overlay: Option<TerrainColumnOverlay>,
414    /// Optional water context for this sampled column.
415    pub water: Option<TerrainColumnWater>,
416    /// Biome loading policy for the render request.
417    pub biome: Option<TerrainColumnBiome>,
418    /// Storage or terrain source that produced this value.
419    pub source: TerrainSampleSource,
420}
421
422#[derive(Debug, Clone, PartialEq)]
423/// Fixed 16x16 terrain column sample grid.
424pub struct TerrainColumnSamples {
425    columns: Vec<Option<TerrainColumnSample>>,
426}
427
428impl TerrainColumnSamples {
429    #[must_use]
430    /// Creates a new value.
431    pub fn new() -> Self {
432        Self {
433            columns: vec![None; 16 * 16],
434        }
435    }
436
437    #[must_use]
438    /// Returns the value at the requested coordinates.
439    pub fn get(&self, local_x: u8, local_z: u8) -> Option<&TerrainColumnSample> {
440        self.columns
441            .get(column_index(local_x, local_z)?)
442            .and_then(Option::as_ref)
443    }
444
445    /// Stores a value at the requested coordinates.
446    pub fn set(&mut self, local_x: u8, local_z: u8, sample: TerrainColumnSample) {
447        if let Some(index) = column_index(local_x, local_z) {
448            if let Some(slot) = self.columns.get_mut(index) {
449                *slot = Some(sample);
450            }
451        }
452    }
453
454    #[must_use]
455    /// Returns the number of populated sampled columns.
456    pub fn sampled_columns(&self) -> usize {
457        self.columns
458            .iter()
459            .filter(|sample| sample.is_some())
460            .count()
461    }
462
463    /// Iterates over populated values.
464    pub fn iter(&self) -> impl Iterator<Item = &TerrainColumnSample> {
465        self.columns.iter().filter_map(Option::as_ref)
466    }
467}
468
469impl Default for TerrainColumnSamples {
470    fn default() -> Self {
471        Self::new()
472    }
473}
474
475#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
476/// Diagnostics collected while loading render chunks.
477pub struct ChunkLoadStats {
478    /// Number of chunks requested by the caller.
479    pub requested_chunks: usize,
480    /// Number of chunks with renderable data loaded.
481    pub loaded_chunks: usize,
482    /// Number of subchunks decoded while loading.
483    pub subchunks_decoded: usize,
484    /// Number of worker threads used by the operation.
485    pub worker_threads: usize,
486    /// Milliseconds spent waiting for bounded pipeline capacity.
487    pub queue_wait_ms: u128,
488    /// Total chunk load time in milliseconds.
489    pub load_ms: u128,
490    /// Number of exact storage keys requested.
491    pub keys_requested: usize,
492    /// Number of requested storage keys found.
493    pub keys_found: usize,
494    /// Number of exact batch-get operations issued.
495    pub exact_get_batches: usize,
496    /// Number of prefix scans issued as fallback or discovery work.
497    pub prefix_scans: usize,
498    /// Milliseconds spent decoding loaded records.
499    pub decode_ms: u128,
500    /// Milliseconds spent reading from the storage backend.
501    pub db_read_ms: u128,
502    /// Milliseconds spent parsing biome records.
503    pub biome_parse_ms: u128,
504    /// Microseconds spent parsing biome records.
505    pub biome_parse_us: u128,
506    /// Milliseconds spent parsing subchunk records.
507    pub subchunk_parse_ms: u128,
508    /// Microseconds spent parsing subchunk records.
509    pub subchunk_parse_us: u128,
510    /// Milliseconds spent computing surface columns.
511    pub surface_scan_ms: u128,
512    /// Microseconds spent computing surface columns.
513    pub surface_scan_us: u128,
514    /// Milliseconds spent parsing block-entity records.
515    pub block_entity_parse_ms: u128,
516    /// Microseconds spent parsing block-entity records.
517    pub block_entity_parse_us: u128,
518    /// Milliseconds spent on full reloads for exact surface requests.
519    pub full_reload_ms: u128,
520    /// Number of legacy terrain records loaded.
521    pub legacy_terrain_records: usize,
522    /// Number of legacy biome samples decoded.
523    pub legacy_biome_samples: usize,
524    /// Compatibility RGB values decoded from legacy biome samples.
525    pub legacy_biome_colors: usize,
526    /// Number of sampled columns sourced from legacy terrain.
527    pub terrain_source_legacy: usize,
528    /// Number of sampled columns sourced from subchunks.
529    pub terrain_source_subchunk: usize,
530    /// Number of virtual legacy chunks loaded from `chunks.dat`.
531    pub legacy_pocket_chunks: usize,
532    /// World format detected during the load.
533    pub detected_format: WorldFormat,
534    /// Number of surface columns computed from block data.
535    pub computed_surface_columns: usize,
536    /// Columns whose raw heightmap disagreed with computed surface data.
537    pub raw_height_mismatch_columns: usize,
538    /// Columns missing required subchunk data.
539    pub missing_subchunk_columns: usize,
540    /// Columns that fell back to legacy terrain data.
541    pub legacy_fallback_columns: usize,
542    /// Columns where legacy RGB biome samples took precedence.
543    pub legacy_biome_preferred_columns: usize,
544    /// Columns where modern biome ids were used as fallback.
545    pub modern_biome_fallback_columns: usize,
546}
547
548#[derive(Debug, Clone)]
549/// Options controlling render chunk loading.
550pub struct ChunkLoadOptions {
551    /// Composable map-data contract requested by the caller.
552    pub data_request: ChunkDataRequest,
553    /// Subchunk decode mode used while loading render data.
554    pub subchunk_decode: SubChunkDecodeMode,
555    /// Threading policy for this operation.
556    pub threading: WorldThreadingOptions,
557    /// Bounded pipeline settings for this operation.
558    pub pipeline: WorldPipelineOptions,
559    /// Optional cancellation flag checked during long-running work.
560    pub cancel: Option<CancelFlag>,
561    /// Optional progress sink invoked during long-running work.
562    pub progress: Option<ProgressSink>,
563    /// Ordering policy for chunk loading.
564    pub priority: ChunkLoadPriority,
565    /// Backend cache strategy for exact storage reads used by render loading.
566    pub storage_cache_policy: StorageCachePolicy,
567}
568
569impl Default for ChunkLoadOptions {
570    fn default() -> Self {
571        Self {
572            data_request: ChunkDataRequest::new()
573                .surface_columns(ExactSurfaceSubchunkPolicy::Full)
574                .biome(BiomeDataRequirement::SurfaceColumns),
575            subchunk_decode: SubChunkDecodeMode::FullIndices,
576            threading: WorldThreadingOptions::Auto,
577            pipeline: WorldPipelineOptions::default(),
578            cancel: None,
579            progress: None,
580            priority: ChunkLoadPriority::RowMajor,
581            storage_cache_policy: StorageCachePolicy::Use,
582        }
583    }
584}
585
586impl ChunkLoadOptions {
587    #[must_use]
588    /// Creates options from an explicit composable map-data contract.
589    pub fn for_data_request(data_request: ChunkDataRequest) -> Self {
590        Self {
591            data_request,
592            ..Self::default()
593        }
594    }
595
596    #[must_use]
597    /// Creates a surface-column load that avoids materializing full 3D palette indices.
598    pub fn exact_surface_columns(
599        subchunks: ExactSurfaceSubchunkPolicy,
600        biome: ExactSurfaceBiomeLoad,
601        block_entities: bool,
602    ) -> Self {
603        Self {
604            data_request: ChunkDataRequest::new()
605                .surface_columns(subchunks)
606                .biome(match biome {
607                    ExactSurfaceBiomeLoad::None => BiomeDataRequirement::None,
608                    ExactSurfaceBiomeLoad::TopColumns => BiomeDataRequirement::SurfaceColumns,
609                    ExactSurfaceBiomeLoad::All => BiomeDataRequirement::All,
610                })
611                .block_entities_if(block_entities),
612            ..Self::default()
613        }
614    }
615
616    #[must_use]
617    /// Creates a raw-height-map load with no subchunk index materialization.
618    pub fn raw_height_map() -> Self {
619        Self {
620            data_request: ChunkDataRequest::new().height_map(),
621            ..Self::default()
622        }
623    }
624
625    #[must_use]
626    /// Creates a fixed-layer load for one world Y coordinate.
627    pub fn layer(y: i32) -> Self {
628        Self {
629            data_request: ChunkDataRequest::new().layer(y),
630            ..Self::default()
631        }
632    }
633
634    #[must_use]
635    /// Creates a biome-only load for one world Y coordinate.
636    pub fn biome(y: i32, load_all: bool) -> Self {
637        Self {
638            data_request: ChunkDataRequest::new().biome(if load_all {
639                BiomeDataRequirement::All
640            } else {
641                BiomeDataRequirement::Layer(y)
642            }),
643            ..Self::default()
644        }
645    }
646}
647
648#[derive(Debug, Clone, PartialEq)]
649/// Block entity included with render chunk data.
650pub struct ChunkBlockEntity {
651    /// Identifier value decoded from storage or NBT.
652    pub id: Option<String>,
653    /// World block position `[x, y, z]` decoded from NBT, when present.
654    pub position: Option<[i32; 3]>,
655    /// Original or parsed Bedrock NBT payload.
656    pub nbt: NbtTag,
657}
658
659#[derive(Debug, Clone, PartialEq)]
660/// Loaded render-oriented chunk data.
661pub struct ChunkData {
662    /// Chunk position represented by this render data.
663    pub pos: ChunkPos,
664    /// Whether enough records were found to treat the chunk as loaded.
665    pub is_loaded: bool,
666    /// Height-map values in Bedrock `z * 16 + x` column order.
667    pub height_map: Option<[[Option<i16>; 16]; 16]>,
668    /// Legacy biome samples decoded from old terrain records.
669    pub legacy_biomes: Option<[[Option<LegacyBiomeSample>; 16]; 16]>,
670    /// Compatibility RGB values decoded from legacy biome samples.
671    pub legacy_biome_colors: Option<[[Option<u32>; 16]; 16]>,
672    /// Parsed biome storage records keyed by vertical section.
673    pub biome_data: BTreeMap<i32, ParsedBiomeStorage>,
674    /// Exact-surface subchunk loading policy.
675    pub subchunks: BTreeMap<i8, SubChunk>,
676    /// Whether block-entity records are loaded with render data.
677    pub block_entities: Vec<ChunkBlockEntity>,
678    /// `LegacyTerrain` record when present for old `LevelDB` worlds.
679    pub legacy_terrain: Option<LegacyTerrain>,
680    /// Canonical surface-column samples computed from actual block data.
681    pub column_samples: Option<TerrainColumnSamples>,
682    /// Bedrock format or payload version.
683    pub version: crate::ChunkVersion,
684}
685
686impl ChunkData {
687    #[must_use]
688    /// Returns the sampled terrain column at local chunk coordinates.
689    pub fn column_sample_at(&self, local_x: u8, local_z: u8) -> Option<&TerrainColumnSample> {
690        self.column_samples.as_ref()?.get(local_x, local_z)
691    }
692}
693
694#[derive(Debug, Clone)]
695struct RawChunkData {
696    pos: ChunkPos,
697    biome_record: Option<(crate::ChunkVersion, Bytes)>,
698    subchunks: BTreeMap<i8, Bytes>,
699    block_entities: Option<Bytes>,
700    legacy_terrain: Option<Bytes>,
701}
702
703#[derive(Debug, Clone, Copy, Default)]
704#[allow(clippy::struct_field_names)]
705struct ChunkDecodeTiming {
706    biome_parse_us: u128,
707    subchunk_parse_us: u128,
708    surface_scan_us: u128,
709    block_entity_parse_us: u128,
710}
711
712impl ChunkDecodeTiming {
713    fn add(&mut self, other: Self) {
714        self.biome_parse_us = self.biome_parse_us.saturating_add(other.biome_parse_us);
715        self.subchunk_parse_us = self
716            .subchunk_parse_us
717            .saturating_add(other.subchunk_parse_us);
718        self.surface_scan_us = self.surface_scan_us.saturating_add(other.surface_scan_us);
719        self.block_entity_parse_us = self
720            .block_entity_parse_us
721            .saturating_add(other.block_entity_parse_us);
722    }
723}
724
725#[derive(Debug, Clone, Copy)]
726enum RenderRecordKind {
727    LegacyTerrain,
728    Data3D,
729    Data2D,
730    Data2DLegacy,
731    Subchunk(i8),
732    BlockEntity,
733}
734
735#[derive(Debug, Clone, Copy)]
736struct RenderRecordRequest {
737    chunk_index: usize,
738    kind: RenderRecordKind,
739}
740
741#[derive(Debug, Clone)]
742/// Options controlling render region loading.
743pub struct WorldChunkQueryRegionLoadOptions {
744    /// Composable map-data contract requested by the caller.
745    pub data_request: ChunkDataRequest,
746    /// Subchunk decode mode used while loading render data.
747    pub subchunk_decode: SubChunkDecodeMode,
748    /// Threading policy for this operation.
749    pub threading: WorldThreadingOptions,
750    /// Bounded pipeline settings for this operation.
751    pub pipeline: WorldPipelineOptions,
752    /// Optional cancellation flag checked during long-running work.
753    pub cancel: Option<CancelFlag>,
754    /// Optional progress sink invoked during long-running work.
755    pub progress: Option<ProgressSink>,
756    /// Ordering policy for chunk loading.
757    pub priority: ChunkLoadPriority,
758    /// Backend cache strategy for exact storage reads used by render loading.
759    pub storage_cache_policy: StorageCachePolicy,
760}
761
762impl Default for WorldChunkQueryRegionLoadOptions {
763    fn default() -> Self {
764        Self {
765            data_request: ChunkLoadOptions::default().data_request,
766            subchunk_decode: SubChunkDecodeMode::FullIndices,
767            threading: WorldThreadingOptions::Auto,
768            pipeline: WorldPipelineOptions::default(),
769            cancel: None,
770            progress: None,
771            priority: ChunkLoadPriority::RowMajor,
772            storage_cache_policy: StorageCachePolicy::Use,
773        }
774    }
775}
776
777impl From<WorldChunkQueryRegionLoadOptions> for ChunkLoadOptions {
778    fn from(options: WorldChunkQueryRegionLoadOptions) -> Self {
779        Self {
780            data_request: options.data_request,
781            subchunk_decode: options.subchunk_decode,
782            threading: options.threading,
783            pipeline: options.pipeline,
784            cancel: options.cancel,
785            progress: options.progress,
786            priority: options.priority,
787            storage_cache_policy: options.storage_cache_policy,
788        }
789    }
790}
791
792impl ChunkDataRequest {
793    fn block_entities_if(mut self, enabled: bool) -> Self {
794        self.block_entities = enabled;
795        self
796    }
797
798    fn preferred_decode_mode(&self) -> SubChunkDecodeMode {
799        if self.subchunks.iter().any(|requirement| {
800            matches!(
801                requirement,
802                SubchunkDataRequirement::Layer(_)
803                    | SubchunkDataRequirement::CaveSlice(_)
804                    | SubchunkDataRequirement::Full3dIndices
805            )
806        }) {
807            SubChunkDecodeMode::FullIndices
808        } else if self
809            .subchunks
810            .iter()
811            .any(|requirement| matches!(requirement, SubchunkDataRequirement::SurfaceColumns(_)))
812        {
813            SubChunkDecodeMode::SurfaceColumns
814        } else {
815            SubChunkDecodeMode::CountsOnly
816        }
817    }
818}
819
820#[derive(Debug, Clone, Copy, PartialEq, Eq)]
821/// Inclusive chunk rectangle to load or scan for rendering.
822pub struct WorldChunkQueryRegion {
823    /// Bedrock dimension covered by this region.
824    pub dimension: crate::Dimension,
825    /// Inclusive minimum chunk X coordinate.
826    pub min_chunk_x: i32,
827    /// Inclusive minimum chunk Z coordinate.
828    pub min_chunk_z: i32,
829    /// Inclusive maximum chunk X coordinate.
830    pub max_chunk_x: i32,
831    /// Inclusive maximum chunk Z coordinate.
832    pub max_chunk_z: i32,
833}
834
835#[derive(Debug, Clone, PartialEq)]
836/// Loaded render region and load diagnostics.
837pub struct WorldChunkQueryRegionData {
838    /// Inclusive chunk region requested by the load.
839    pub region: WorldChunkQueryRegion,
840    /// Parsed or loaded chunks in this result.
841    pub chunks: Vec<ChunkData>,
842    /// Load diagnostics and timing counters.
843    pub stats: ChunkLoadStats,
844}
845
846#[derive(Debug, Clone, Copy, PartialEq, Eq)]
847/// Inclusive chunk bounds discovered in a world.
848pub struct ChunkBounds {
849    /// Bedrock dimension covered by these bounds.
850    pub dimension: crate::Dimension,
851    /// Inclusive minimum chunk X coordinate.
852    pub min_chunk_x: i32,
853    /// Inclusive minimum chunk Z coordinate.
854    pub min_chunk_z: i32,
855    /// Inclusive maximum chunk X coordinate.
856    pub max_chunk_x: i32,
857    /// Inclusive maximum chunk Z coordinate.
858    pub max_chunk_z: i32,
859    /// Number of chunks represented by these bounds.
860    pub chunk_count: usize,
861}
862
863impl ChunkBounds {
864    fn from_first(pos: ChunkPos) -> Self {
865        Self {
866            dimension: pos.dimension,
867            min_chunk_x: pos.x,
868            min_chunk_z: pos.z,
869            max_chunk_x: pos.x,
870            max_chunk_z: pos.z,
871            chunk_count: 1,
872        }
873    }
874
875    fn include(&mut self, pos: ChunkPos) {
876        self.min_chunk_x = self.min_chunk_x.min(pos.x);
877        self.min_chunk_z = self.min_chunk_z.min(pos.z);
878        self.max_chunk_x = self.max_chunk_x.max(pos.x);
879        self.max_chunk_z = self.max_chunk_z.max(pos.z);
880        self.chunk_count = self.chunk_count.saturating_add(1);
881    }
882}
883
884#[derive(Debug, Clone)]
885/// Options controlling world scan operations.
886pub struct WorldScanOptions {
887    /// Threading policy for this operation.
888    pub threading: WorldThreadingOptions,
889    /// Bounded pipeline settings for this operation.
890    pub pipeline: WorldPipelineOptions,
891    /// Optional cancellation flag checked during long-running work.
892    pub cancel: Option<CancelFlag>,
893    /// Optional progress sink invoked during long-running work.
894    pub progress: Option<ProgressSink>,
895}
896
897impl Default for WorldScanOptions {
898    fn default() -> Self {
899        Self {
900            threading: WorldThreadingOptions::Auto,
901            pipeline: WorldPipelineOptions::default(),
902            cancel: None,
903            progress: None,
904        }
905    }
906}
907
908#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
909/// Threading policy for world-level operations.
910pub enum WorldThreadingOptions {
911    #[default]
912    /// Automatically choose the appropriate mode.
913    Auto,
914    /// Use a fixed worker count.
915    Fixed(usize),
916    /// Use a single worker.
917    Single,
918}
919
920/// max world threads constant.
921pub const MAX_WORLD_THREADS: usize = 512;
922
923impl WorldThreadingOptions {
924    #[must_use]
925    /// Resolves this policy to an effective worker count.
926    pub fn resolve(self, work_items: usize) -> usize {
927        self.resolve_unchecked(work_items)
928    }
929
930    #[must_use]
931    /// Resolves this policy without reporting validation errors.
932    pub fn resolve_unchecked(self, work_items: usize) -> usize {
933        match self {
934            Self::Single => 1,
935            Self::Fixed(threads) => threads.clamp(1, MAX_WORLD_THREADS),
936            Self::Auto => std::thread::available_parallelism()
937                .map_or(1, usize::from)
938                .min(work_items.max(1)),
939        }
940    }
941
942    /// Resolves this policy and validates explicit worker counts.
943    pub fn resolve_checked(self, work_items: usize) -> Result<usize> {
944        match self {
945            Self::Fixed(0) => Err(BedrockWorldError::Validation(
946                "thread count must be in 1..=512".to_string(),
947            )),
948            Self::Fixed(threads) if threads > MAX_WORLD_THREADS => Err(
949                BedrockWorldError::Validation("thread count must be in 1..=512".to_string()),
950            ),
951            _ => Ok(self.resolve_unchecked(work_items)),
952        }
953    }
954}
955
956#[derive(Debug, Clone, Default)]
957/// Shareable cancellation flag for world operations.
958pub struct CancelFlag(Arc<AtomicBool>);
959
960impl CancelFlag {
961    #[must_use]
962    /// Creates a new uncancelled flag.
963    pub fn new() -> Self {
964        Self::default()
965    }
966
967    /// Requests cancellation for operations sharing this flag.
968    pub fn cancel(&self) {
969        self.0.store(true, Ordering::Relaxed);
970    }
971
972    #[must_use]
973    /// Creates a flag from a shared atomic cancellation marker.
974    pub fn from_shared(cancelled: Arc<AtomicBool>) -> Self {
975        Self(cancelled)
976    }
977
978    #[must_use]
979    /// Converts this flag into a storage-layer cancellation flag.
980    pub fn to_storage_cancel(&self) -> StorageCancelFlag {
981        StorageCancelFlag::from_shared(Arc::clone(&self.0))
982    }
983
984    #[must_use]
985    /// Returns whether cancellation has been requested.
986    pub fn is_cancelled(&self) -> bool {
987        self.0.load(Ordering::Relaxed)
988    }
989}
990
991#[derive(Clone)]
992/// Callback sink for world scan progress.
993pub struct ProgressSink {
994    inner: Arc<Mutex<Box<dyn FnMut(WorldScanProgress) + Send>>>,
995}
996
997impl std::fmt::Debug for ProgressSink {
998    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
999        formatter
1000            .debug_struct("ProgressSink")
1001            .finish_non_exhaustive()
1002    }
1003}
1004
1005impl ProgressSink {
1006    #[must_use]
1007    /// Creates a progress sink from a callback invoked during scans.
1008    pub fn new(callback: impl FnMut(WorldScanProgress) + Send + 'static) -> Self {
1009        Self {
1010            inner: Arc::new(Mutex::new(Box::new(callback))),
1011        }
1012    }
1013
1014    fn emit(&self, progress: WorldScanProgress) {
1015        if let Ok(mut callback) = self.inner.lock() {
1016            callback(progress);
1017        }
1018    }
1019}
1020
1021#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1022/// Progress update emitted during world scans.
1023pub struct WorldScanProgress {
1024    /// Number of entries observed when progress was emitted.
1025    pub entries_seen: usize,
1026}
1027
1028impl BedrockWorld<Arc<dyn WorldStorage>> {
1029    /// Opens a world on the calling thread with automatic format detection.
1030    pub fn open_blocking(path: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
1031        let path = path.as_ref().to_path_buf();
1032        let format = detect_world_format(&path, options.format)?;
1033        let storage: Arc<dyn WorldStorage> = match format {
1034            WorldFormat::LevelDb | WorldFormat::LevelDbLegacyTerrain => {
1035                let db_path = path.join("db");
1036                if options.read_only {
1037                    Arc::new(BedrockLevelDbStorage::open_read_only(db_path)?)
1038                } else {
1039                    Arc::new(BedrockLevelDbStorage::open(db_path)?)
1040                }
1041            }
1042            WorldFormat::PocketChunksDat => {
1043                if !options.read_only {
1044                    log::warn!(
1045                        "opening legacy chunks.dat world as read-only despite read_only=false"
1046                    );
1047                }
1048                Arc::new(PocketChunksDatStorage::open(&path)?)
1049            }
1050        };
1051        log::debug!(
1052            "opened Bedrock world (path={}, format={:?}, read_only={})",
1053            path.display(),
1054            format,
1055            options.read_only
1056        );
1057        Ok(Self {
1058            path,
1059            options,
1060            storage,
1061            format,
1062        })
1063    }
1064
1065    #[cfg(feature = "async")]
1066    /// Opens a world on a blocking worker thread and returns an async handle.
1067    pub async fn open(path: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
1068        let path = path.as_ref().to_path_buf();
1069        tokio::task::spawn_blocking(move || Self::open_blocking(path, options))
1070            .await
1071            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
1072    }
1073
1074    #[must_use]
1075    /// Creates a world handle from an already-open storage backend.
1076    pub fn from_storage(
1077        path: impl Into<PathBuf>,
1078        storage: Arc<dyn WorldStorage>,
1079        options: OpenOptions,
1080    ) -> Self {
1081        Self {
1082            path: path.into(),
1083            options,
1084            storage,
1085            format: WorldFormat::LevelDb,
1086        }
1087    }
1088
1089    #[must_use]
1090    /// Creates a world handle from an already-open storage backend and explicit format.
1091    pub fn from_storage_with_format(
1092        path: impl Into<PathBuf>,
1093        storage: Arc<dyn WorldStorage>,
1094        options: OpenOptions,
1095        format: WorldFormat,
1096    ) -> Self {
1097        Self {
1098            path: path.into(),
1099            options,
1100            storage,
1101            format,
1102        }
1103    }
1104}
1105
1106impl BedrockWorld<BedrockLevelDbStorage> {
1107    /// Opens a world with a concrete `BedrockLevelDbStorage` backend on the calling thread.
1108    pub fn open_typed_blocking(path: impl AsRef<Path>, options: OpenOptions) -> Result<Self> {
1109        let path = path.as_ref().to_path_buf();
1110        let format = detect_world_format(&path, options.format)?;
1111        match format {
1112            WorldFormat::LevelDb | WorldFormat::LevelDbLegacyTerrain => {
1113                let db_path = path.join("db");
1114                let storage = if options.read_only {
1115                    BedrockLevelDbStorage::open_read_only(db_path)?
1116                } else {
1117                    BedrockLevelDbStorage::open(db_path)?
1118                };
1119                Ok(Self {
1120                    path,
1121                    options,
1122                    storage,
1123                    format,
1124                })
1125            }
1126            WorldFormat::PocketChunksDat => Err(BedrockWorldError::UnsupportedChunkFormat(
1127                "typed LevelDB open does not support legacy chunks.dat worlds".to_string(),
1128            )),
1129        }
1130    }
1131}
1132
1133impl<S> BedrockWorld<S>
1134where
1135    S: WorldStorageHandle,
1136{
1137    #[must_use]
1138    /// Creates a world handle from a concrete storage backend.
1139    pub fn from_typed_storage(path: impl Into<PathBuf>, storage: S, options: OpenOptions) -> Self {
1140        Self {
1141            path: path.into(),
1142            options,
1143            storage,
1144            format: WorldFormat::LevelDb,
1145        }
1146    }
1147
1148    #[must_use]
1149    /// Creates a world handle from a concrete storage backend and explicit format.
1150    pub fn from_typed_storage_with_format(
1151        path: impl Into<PathBuf>,
1152        storage: S,
1153        options: OpenOptions,
1154        format: WorldFormat,
1155    ) -> Self {
1156        Self {
1157            path: path.into(),
1158            options,
1159            storage,
1160            format,
1161        }
1162    }
1163
1164    #[must_use]
1165    /// Returns the underlying raw storage backend.
1166    pub fn storage(&self) -> &dyn WorldStorage {
1167        self.storage.storage()
1168    }
1169
1170    #[must_use]
1171    /// Returns the world folder path.
1172    pub fn path(&self) -> &Path {
1173        &self.path
1174    }
1175
1176    #[must_use]
1177    /// Returns the detected world storage format.
1178    pub const fn format(&self) -> WorldFormat {
1179        self.format
1180    }
1181
1182    /// Read level dat blocking.
1183    pub fn read_level_dat_blocking(&self) -> Result<LevelDatDocument> {
1184        read_level_dat_document(&self.path.join("level.dat"))
1185    }
1186
1187    /// Write level dat blocking.
1188    pub fn write_level_dat_blocking(&self, document: &LevelDatDocument) -> Result<()> {
1189        self.ensure_writable()?;
1190        write_level_dat_document(&self.path.join("level.dat"), document)
1191    }
1192
1193    /// Compact the underlying world storage after writes.
1194    pub fn compact_storage_blocking(&self) -> Result<()> {
1195        self.ensure_writable()?;
1196        self.storage().compact()
1197    }
1198
1199    /// List players blocking.
1200    pub fn list_players_blocking(&self) -> Result<Vec<PlayerId>> {
1201        let mut players = Vec::new();
1202        if self.storage().get(b"~local_player")?.is_some() {
1203            players.push(PlayerId::Local);
1204        }
1205        self.storage().for_each_prefix_key(
1206            b"player_",
1207            StorageReadOptions::default(),
1208            &mut |key| {
1209                if let Some(player) = PlayerId::from_storage_key(key) {
1210                    players.push(player);
1211                }
1212                Ok(StorageVisitorControl::Continue)
1213            },
1214        )?;
1215        Ok(players)
1216    }
1217
1218    /// Classify keys blocking.
1219    pub fn classify_keys_blocking(
1220        &self,
1221        options: WorldScanOptions,
1222    ) -> Result<BTreeMap<String, usize>> {
1223        let mut counts = BTreeMap::new();
1224        let mut entries_seen = 0usize;
1225        self.storage()
1226            .for_each_key(to_storage_read_options(&options), &mut |key| {
1227                check_cancelled(&options)?;
1228                entries_seen = entries_seen.saturating_add(1);
1229                if entries_seen.is_multiple_of(8192) {
1230                    emit_progress(&options, entries_seen);
1231                }
1232                let key = BedrockDbKey::decode(key);
1233                *counts.entry(key.summary_kind()).or_default() += 1;
1234                Ok(StorageVisitorControl::Continue)
1235            })?;
1236        emit_progress(&options, entries_seen);
1237        Ok(counts)
1238    }
1239
1240    /// List chunk positions blocking.
1241    pub fn list_chunk_positions_blocking(
1242        &self,
1243        options: WorldScanOptions,
1244    ) -> Result<Vec<ChunkPos>> {
1245        let mut positions = BTreeSet::new();
1246        let mut entries_seen = 0usize;
1247        self.storage()
1248            .for_each_key(to_storage_read_options(&options), &mut |key| {
1249                check_cancelled(&options)?;
1250                entries_seen = entries_seen.saturating_add(1);
1251                if let BedrockDbKey::Chunk(chunk_key) = BedrockDbKey::decode(key) {
1252                    positions.insert(chunk_key.pos);
1253                }
1254                if entries_seen.is_multiple_of(8192) {
1255                    emit_progress(&options, entries_seen);
1256                }
1257                Ok(StorageVisitorControl::Continue)
1258            })?;
1259        Ok(positions.into_iter().collect())
1260    }
1261
1262    /// List render chunk positions blocking.
1263    pub fn list_render_chunk_positions_blocking(
1264        &self,
1265        options: WorldScanOptions,
1266    ) -> Result<Vec<ChunkPos>> {
1267        let started = Instant::now();
1268        log::debug!(
1269            "listing render chunk positions (threading={:?}, queue_depth={}, progress_interval={})",
1270            options.threading,
1271            options.pipeline.queue_depth,
1272            options.pipeline.progress_interval
1273        );
1274        let mut positions = BTreeSet::new();
1275        let mut entries_seen = 0usize;
1276        let outcome =
1277            self.storage()
1278                .for_each_key(to_storage_read_options(&options), &mut |key| {
1279                    check_cancelled(&options)?;
1280                    entries_seen = entries_seen.saturating_add(1);
1281                    if let BedrockDbKey::Chunk(chunk_key) = BedrockDbKey::decode(key) {
1282                        if chunk_key.tag.is_render_chunk_record() {
1283                            positions.insert(chunk_key.pos);
1284                        }
1285                    }
1286                    if entries_seen.is_multiple_of(8192) {
1287                        emit_progress(&options, entries_seen);
1288                    }
1289                    Ok(StorageVisitorControl::Continue)
1290                })?;
1291        let positions = positions.into_iter().collect::<Vec<_>>();
1292        log::debug!(
1293            "render chunk position listing complete (entries_seen={}, positions={}, visited={}, tables_scanned={}, worker_threads={}, queue_wait_ms={}, cancel_checks={}, elapsed_ms={})",
1294            entries_seen,
1295            positions.len(),
1296            outcome.visited,
1297            outcome.tables_scanned,
1298            outcome.worker_threads,
1299            outcome.queue_wait_ms,
1300            outcome.cancel_checks,
1301            started.elapsed().as_millis()
1302        );
1303        Ok(positions)
1304    }
1305
1306    #[allow(clippy::too_many_lines)]
1307    /// List render chunk positions in region blocking.
1308    pub fn list_chunk_positions_in_region_blocking(
1309        &self,
1310        region: WorldChunkQueryRegion,
1311        options: WorldScanOptions,
1312    ) -> Result<Vec<ChunkPos>> {
1313        let started = Instant::now();
1314        validate_render_region(region)?;
1315        let x_count = i64::from(region.max_chunk_x) - i64::from(region.min_chunk_x) + 1;
1316        let z_count = i64::from(region.max_chunk_z) - i64::from(region.min_chunk_z) + 1;
1317        let capacity = usize::try_from(x_count.saturating_mul(z_count))
1318            .map_err(|_| BedrockWorldError::Validation("render region is too large".to_string()))?;
1319        let mut positions = Vec::with_capacity(capacity);
1320        for z in region.min_chunk_z..=region.max_chunk_z {
1321            for x in region.min_chunk_x..=region.max_chunk_x {
1322                positions.push(ChunkPos {
1323                    x,
1324                    z,
1325                    dimension: region.dimension,
1326                });
1327            }
1328        }
1329        if positions.is_empty() {
1330            return Ok(Vec::new());
1331        }
1332
1333        let worker_count = options.threading.resolve_checked(positions.len())?;
1334        log::debug!(
1335            "indexing render chunk region (dimension={:?}, min=({}, {}), max=({}, {}), workers={})",
1336            region.dimension,
1337            region.min_chunk_x,
1338            region.min_chunk_z,
1339            region.max_chunk_x,
1340            region.max_chunk_z,
1341            worker_count
1342        );
1343        if worker_count == 1 {
1344            let render_positions = positions
1345                .into_iter()
1346                .filter_map(
1347                    |pos| match self.has_render_chunk_records_blocking(pos, &options) {
1348                        Ok(true) => Some(Ok(pos)),
1349                        Ok(false) => None,
1350                        Err(error) => Some(Err(error)),
1351                    },
1352                )
1353                .collect::<Result<Vec<_>>>()?;
1354            log::debug!(
1355                "render chunk region index complete (dimension={:?}, candidates={}, positions={}, workers={}, queue_depth=0, elapsed_ms={})",
1356                region.dimension,
1357                capacity,
1358                render_positions.len(),
1359                worker_count,
1360                started.elapsed().as_millis()
1361            );
1362            return Ok(render_positions);
1363        }
1364
1365        let scan_options = WorldScanOptions {
1366            threading: WorldThreadingOptions::Single,
1367            pipeline: options.pipeline,
1368            cancel: options.cancel.clone(),
1369            progress: options.progress.clone(),
1370        };
1371        let next_position = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1372        let queue_depth = options
1373            .pipeline
1374            .resolve_queue_depth(worker_count, positions.len());
1375        let (sender, receiver) = mpsc::sync_channel::<Result<Option<ChunkPos>>>(queue_depth);
1376        let pool = world_pool(worker_count)?;
1377        pool.scope(|scope| {
1378            for worker_index in 0..worker_count {
1379                let next_position = Arc::clone(&next_position);
1380                let sender = sender.clone();
1381                let positions = &positions;
1382                let scan_options = scan_options.clone();
1383                scope.spawn(move |_| {
1384                    log::trace!("render region index worker {worker_index} started");
1385                    loop {
1386                        if scan_options
1387                            .cancel
1388                            .as_ref()
1389                            .is_some_and(CancelFlag::is_cancelled)
1390                        {
1391                            return;
1392                        }
1393                        let index = next_position.fetch_add(1, Ordering::Relaxed);
1394                        let Some(pos) = positions.get(index).copied() else {
1395                            log::trace!("render region index worker {worker_index} finished");
1396                            return;
1397                        };
1398                        let result = self
1399                            .has_render_chunk_records_blocking(pos, &scan_options)
1400                            .map(|is_renderable| is_renderable.then_some(pos));
1401                        if sender.send(result).is_err() {
1402                            return;
1403                        }
1404                    }
1405                });
1406            }
1407            drop(sender);
1408
1409            let mut render_positions = Vec::new();
1410            for result in receiver {
1411                if let Some(pos) = result? {
1412                    render_positions.push(pos);
1413                }
1414            }
1415            render_positions.sort();
1416            log::debug!(
1417                "render chunk region index complete (dimension={:?}, candidates={}, positions={}, workers={}, queue_depth={}, elapsed_ms={})",
1418                region.dimension,
1419                positions.len(),
1420                render_positions.len(),
1421                worker_count,
1422                queue_depth,
1423                started.elapsed().as_millis()
1424            );
1425            Ok(render_positions)
1426        })
1427    }
1428
1429    /// Discover chunk bounds blocking.
1430    pub fn discover_chunk_bounds_blocking(
1431        &self,
1432        dimension: crate::Dimension,
1433        options: WorldScanOptions,
1434    ) -> Result<Option<ChunkBounds>> {
1435        let mut bounds: Option<ChunkBounds> = None;
1436        let mut seen_positions = BTreeSet::new();
1437        let mut entries_seen = 0usize;
1438        self.storage()
1439            .for_each_key(to_storage_read_options(&options), &mut |key| {
1440                check_cancelled(&options)?;
1441                entries_seen = entries_seen.saturating_add(1);
1442                if let BedrockDbKey::Chunk(chunk_key) = BedrockDbKey::decode(key) {
1443                    if chunk_key.pos.dimension == dimension && seen_positions.insert(chunk_key.pos)
1444                    {
1445                        match &mut bounds {
1446                            Some(bounds) => bounds.include(chunk_key.pos),
1447                            None => bounds = Some(ChunkBounds::from_first(chunk_key.pos)),
1448                        }
1449                    }
1450                }
1451                if entries_seen.is_multiple_of(8192) {
1452                    emit_progress(&options, entries_seen);
1453                }
1454                Ok(StorageVisitorControl::Continue)
1455            })?;
1456        Ok(bounds)
1457    }
1458
1459    /// Nearest loaded chunk to spawn blocking.
1460    pub fn nearest_loaded_chunk_to_spawn_blocking(
1461        &self,
1462        dimension: crate::Dimension,
1463        spawn_block_x: i32,
1464        spawn_block_z: i32,
1465        options: WorldScanOptions,
1466    ) -> Result<Option<ChunkPos>> {
1467        let spawn_chunk = BlockPos {
1468            x: spawn_block_x,
1469            y: 0,
1470            z: spawn_block_z,
1471        }
1472        .to_chunk_pos(dimension);
1473        let mut best = None::<(i64, ChunkPos)>;
1474        let mut seen_positions = BTreeSet::new();
1475        let mut entries_seen = 0usize;
1476        self.storage()
1477            .for_each_key(to_storage_read_options(&options), &mut |key| {
1478                check_cancelled(&options)?;
1479                entries_seen = entries_seen.saturating_add(1);
1480                if let BedrockDbKey::Chunk(chunk_key) = BedrockDbKey::decode(key) {
1481                    if chunk_key.pos.dimension == dimension && seen_positions.insert(chunk_key.pos)
1482                    {
1483                        let dx = i64::from(chunk_key.pos.x) - i64::from(spawn_chunk.x);
1484                        let dz = i64::from(chunk_key.pos.z) - i64::from(spawn_chunk.z);
1485                        let distance = dx.saturating_mul(dx).saturating_add(dz.saturating_mul(dz));
1486                        if best.is_none_or(|(best_distance, _)| distance < best_distance) {
1487                            best = Some((distance, chunk_key.pos));
1488                        }
1489                    }
1490                }
1491                if entries_seen.is_multiple_of(8192) {
1492                    emit_progress(&options, entries_seen);
1493                }
1494                Ok(StorageVisitorControl::Continue)
1495            })?;
1496        Ok(best.map(|(_, pos)| pos))
1497    }
1498
1499    /// Get player blocking.
1500    pub fn get_player_blocking(&self, id: &PlayerId) -> Result<Option<PlayerData>> {
1501        let Some(key) = id.storage_key() else {
1502            if *id == PlayerId::LegacyLevelDat {
1503                let document = self.read_level_dat_blocking()?;
1504                return Ok(Some(PlayerData::from_nbt(id.clone(), document.root)?));
1505            }
1506            return Ok(None);
1507        };
1508        self.storage()
1509            .get(key.as_ref())?
1510            .map(|bytes| PlayerData::from_raw(id.clone(), bytes))
1511            .transpose()
1512    }
1513
1514    /// Put player blocking.
1515    pub fn put_player_blocking(&self, player: &PlayerData) -> Result<()> {
1516        self.ensure_writable()?;
1517        let Some(key) = player.id.storage_key() else {
1518            return Err(BedrockWorldError::Validation(
1519                "player id has no LevelDB key".to_string(),
1520            ));
1521        };
1522        self.storage().put(key.as_ref(), &player.raw)
1523    }
1524
1525    /// Get chunk blocking.
1526    pub fn get_chunk_blocking(&self, pos: ChunkPos) -> Result<Chunk> {
1527        let mut records = Vec::new();
1528        let prefix = chunk_record_prefix(pos);
1529        self.storage().for_each_prefix(
1530            &prefix,
1531            StorageReadOptions::default(),
1532            &mut |raw_key, value| {
1533                if let Ok(key) = ChunkKey::decode(raw_key) {
1534                    if key.pos == pos {
1535                        records.push(ChunkRecord {
1536                            key,
1537                            value: value.clone(),
1538                        });
1539                    }
1540                }
1541                Ok(StorageVisitorControl::Continue)
1542            },
1543        )?;
1544        let version = records
1545            .iter()
1546            .find(|record| record.key.tag == ChunkRecordTag::Version)
1547            .and_then(|record| record.value.first().copied());
1548        Ok(Chunk {
1549            pos,
1550            version,
1551            records,
1552        })
1553    }
1554
1555    /// Reads and decodes a subchunk on the calling thread.
1556    pub fn get_subchunk_blocking(&self, pos: ChunkPos, y: i8) -> Result<Option<crate::SubChunk>> {
1557        self.get_chunk_blocking(pos)?.get_subchunk(y)
1558    }
1559
1560    /// Parses the world on the calling thread using the selected retention options.
1561    pub fn parse_world_blocking(&self, options: WorldParseOptions) -> Result<ParsedWorld> {
1562        let level_dat = self.read_level_dat_blocking()?;
1563        parse_world_storage(level_dat, self.storage(), options)
1564    }
1565
1566    /// Parses all known records for one chunk on the calling thread.
1567    pub fn parse_chunk_blocking(&self, pos: ChunkPos) -> Result<ParsedChunkData> {
1568        let chunk = self.get_chunk_blocking(pos)?;
1569        Ok(parse_chunk_records(pos, chunk.records))
1570    }
1571
1572    /// Parses one chunk on the calling thread using custom parse options.
1573    pub fn parse_chunk_with_options_blocking(
1574        &self,
1575        pos: ChunkPos,
1576        options: WorldParseOptions,
1577    ) -> Result<ParsedChunkData> {
1578        let chunk = self.get_chunk_blocking(pos)?;
1579        Ok(parse_chunk_records_with_options(
1580            pos,
1581            chunk.records,
1582            options,
1583        ))
1584    }
1585
1586    /// Parse subchunk blocking.
1587    pub fn parse_subchunk_blocking(
1588        &self,
1589        pos: ChunkPos,
1590        y: i8,
1591        options: WorldParseOptions,
1592    ) -> Result<Option<crate::SubChunk>> {
1593        let key = ChunkKey::subchunk(pos, y);
1594        self.storage()
1595            .get(&key.encode())?
1596            .map(|value| parse_subchunk_with_mode(y, value, options.subchunk_decode_mode))
1597            .transpose()
1598    }
1599
1600    /// Get biome storage blocking.
1601    pub fn get_biome_storage_blocking(
1602        &self,
1603        pos: ChunkPos,
1604        y: i32,
1605    ) -> Result<Option<ParsedBiomeStorage>> {
1606        let Some(biome_data) = self.get_biome_data_blocking(pos)? else {
1607            return Ok(None);
1608        };
1609        for storage in biome_data.storages {
1610            if biome_storage_contains_y(&storage, y) {
1611                return Ok(Some(storage));
1612            }
1613        }
1614        Ok(None)
1615    }
1616
1617    /// Get biome storages blocking.
1618    pub fn get_biome_storages_blocking(
1619        &self,
1620        pos: ChunkPos,
1621    ) -> Result<Option<Vec<ParsedBiomeStorage>>> {
1622        Ok(self
1623            .get_biome_data_blocking(pos)?
1624            .map(|biome_data| biome_data.storages))
1625    }
1626
1627    fn get_biome_data_blocking(&self, pos: ChunkPos) -> Result<Option<ParsedBiomeData>> {
1628        for (tag, version) in [
1629            (ChunkRecordTag::Data3D, crate::ChunkVersion::New),
1630            (ChunkRecordTag::Data2D, crate::ChunkVersion::Old),
1631            (ChunkRecordTag::Data2DLegacy, crate::ChunkVersion::Old),
1632        ] {
1633            let key = ChunkKey::new(pos, tag).encode();
1634            let Some(value) = self.storage().get(&key)? else {
1635                continue;
1636            };
1637            let biome_data = match version {
1638                crate::ChunkVersion::New => parse_data3d(&value),
1639                crate::ChunkVersion::Old => parse_legacy_data2d(&value),
1640            }
1641            .map_err(|error| BedrockWorldError::CorruptWorld(format!("biome data: {error}")))?;
1642            return Ok(Some(biome_data));
1643        }
1644        Ok(None)
1645    }
1646
1647    fn has_render_chunk_records_blocking(
1648        &self,
1649        pos: ChunkPos,
1650        options: &WorldScanOptions,
1651    ) -> Result<bool> {
1652        let prefix = chunk_record_prefix(pos);
1653        let mut found = false;
1654        self.storage().for_each_prefix_key(
1655            &prefix,
1656            to_storage_read_options(options),
1657            &mut |key| {
1658                check_cancelled(options)?;
1659                if let BedrockDbKey::Chunk(chunk_key) = BedrockDbKey::decode(key) {
1660                    if chunk_key.pos == pos && chunk_key.tag.is_render_chunk_record() {
1661                        found = true;
1662                        return Ok(StorageVisitorControl::Stop);
1663                    }
1664                }
1665                Ok(StorageVisitorControl::Continue)
1666            },
1667        )?;
1668        Ok(found)
1669    }
1670
1671    /// Get height at blocking.
1672    pub fn get_height_at_blocking(
1673        &self,
1674        pos: ChunkPos,
1675        local_x: u8,
1676        local_z: u8,
1677    ) -> Result<Option<i16>> {
1678        validate_local_column(local_x, local_z)?;
1679        Ok(self
1680            .get_height_map_blocking(pos)?
1681            .and_then(|heights| heights[usize::from(local_z)][usize::from(local_x)]))
1682    }
1683
1684    /// Get height map blocking.
1685    pub fn get_height_map_blocking(
1686        &self,
1687        pos: ChunkPos,
1688    ) -> Result<Option<[[Option<i16>; 16]; 16]>> {
1689        if let Some(biome_data) = self
1690            .get_biome_data_blocking(pos)
1691            .map_err(|error| BedrockWorldError::CorruptWorld(format!("height data: {error}")))?
1692        {
1693            return Ok(Some(render_height_map_from_biome_data(pos, &biome_data)));
1694        }
1695        let key = ChunkKey::new(pos, ChunkRecordTag::LegacyTerrain).encode();
1696        if let Some(value) = self.storage().get(&key)? {
1697            let terrain = LegacyTerrain::parse(value)?;
1698            return Ok(Some(render_height_map_from_legacy_terrain(&terrain)));
1699        }
1700        Ok(None)
1701    }
1702
1703    /// Get legacy biome colors blocking.
1704    pub fn get_legacy_biome_colors_blocking(
1705        &self,
1706        pos: ChunkPos,
1707    ) -> Result<Option<[[Option<u32>; 16]; 16]>> {
1708        let key = ChunkKey::new(pos, ChunkRecordTag::LegacyTerrain).encode();
1709        let Some(value) = self.storage().get(&key)? else {
1710            return Ok(None);
1711        };
1712        let terrain = LegacyTerrain::parse(value)?;
1713        Ok(Some(render_biome_colors_from_legacy_terrain(&terrain)))
1714    }
1715
1716    /// Get legacy biome samples blocking.
1717    pub fn get_legacy_biome_samples_blocking(
1718        &self,
1719        pos: ChunkPos,
1720    ) -> Result<Option<[[Option<LegacyBiomeSample>; 16]; 16]>> {
1721        let key = ChunkKey::new(pos, ChunkRecordTag::LegacyTerrain).encode();
1722        let Some(value) = self.storage().get(&key)? else {
1723            return Ok(None);
1724        };
1725        let terrain = LegacyTerrain::parse(value)?;
1726        Ok(Some(render_biomes_from_legacy_terrain(&terrain)))
1727    }
1728
1729    /// Get legacy biome color blocking.
1730    pub fn get_legacy_biome_color_blocking(
1731        &self,
1732        pos: ChunkPos,
1733        local_x: u8,
1734        local_z: u8,
1735    ) -> Result<Option<u32>> {
1736        validate_local_column(local_x, local_z)?;
1737        Ok(self
1738            .get_legacy_biome_colors_blocking(pos)?
1739            .and_then(|colors| colors[usize::from(local_z)][usize::from(local_x)]))
1740    }
1741
1742    /// Get legacy biome sample blocking.
1743    pub fn get_legacy_biome_sample_blocking(
1744        &self,
1745        pos: ChunkPos,
1746        local_x: u8,
1747        local_z: u8,
1748    ) -> Result<Option<LegacyBiomeSample>> {
1749        validate_local_column(local_x, local_z)?;
1750        Ok(self
1751            .get_legacy_biome_samples_blocking(pos)?
1752            .and_then(|samples| samples[usize::from(local_z)][usize::from(local_x)]))
1753    }
1754
1755    /// Get biome id blocking.
1756    pub fn get_biome_id_blocking(
1757        &self,
1758        pos: ChunkPos,
1759        local_x: u8,
1760        local_z: u8,
1761        y: i32,
1762    ) -> Result<Option<u32>> {
1763        validate_local_column(local_x, local_z)?;
1764        let Some(storage) = self.get_biome_storage_blocking(pos, y)? else {
1765            return Ok(None);
1766        };
1767        Ok(biome_id_from_storage(&storage, local_x, local_z, y))
1768    }
1769
1770    /// Get surface column blocking.
1771    pub fn get_surface_column_blocking(
1772        &self,
1773        pos: ChunkPos,
1774        local_x: u8,
1775        local_z: u8,
1776        options: SurfaceColumnOptions,
1777    ) -> Result<Option<SurfaceColumn>> {
1778        validate_local_column(local_x, local_z)?;
1779        let (min_y, max_y) = pos.y_range(crate::ChunkVersion::New);
1780        let start_y = match self.get_height_at_blocking(pos, local_x, local_z)? {
1781            Some(height) => i32::from(height).clamp(min_y, max_y),
1782            None => return Ok(None),
1783        };
1784        for y in (min_y..=start_y).rev() {
1785            let Some(block) = self.block_state_in_chunk_column(pos, local_x, y, local_z)? else {
1786                continue;
1787            };
1788            if options.skip_air && is_air_block_name(&block.name) {
1789                continue;
1790            }
1791            let biome_id = self.get_biome_id_blocking(pos, local_x, local_z, y)?;
1792            let (water_depth, under_water_block_name) =
1793                if options.transparent_water && is_water_block_name(&block.name) {
1794                    self.find_solid_under_water(pos, local_x, local_z, y, min_y)?
1795                } else {
1796                    (0, None)
1797                };
1798            return Ok(Some(SurfaceColumn {
1799                y,
1800                block_name: block.name,
1801                biome_id,
1802                water_depth,
1803                under_water_block_name,
1804                is_fallback: false,
1805            }));
1806        }
1807        Ok(None)
1808    }
1809
1810    /// Load render chunk blocking.
1811    pub fn query_chunk_data_blocking(
1812        &self,
1813        pos: ChunkPos,
1814        options: ChunkLoadOptions,
1815    ) -> Result<ChunkData> {
1816        let (mut chunks, _) = self.query_chunk_data_with_stats_blocking([pos], options)?;
1817        chunks.pop().ok_or_else(|| {
1818            BedrockWorldError::CorruptWorld("exact render load returned no chunk".to_string())
1819        })
1820    }
1821
1822    /// Loads only canonical terrain column samples for one chunk.
1823    ///
1824    /// The request remains configurable for subchunk, biome, block-entity, storage,
1825    /// cancellation, and threading policy, but this entry point always retains packed
1826    /// palette indices rather than materializing full 3D index arrays.
1827    pub fn load_surface_columns_blocking(
1828        &self,
1829        pos: ChunkPos,
1830        mut options: ChunkLoadOptions,
1831    ) -> Result<Option<TerrainColumnSamples>> {
1832        let mut request = options.data_request.clone();
1833        if !request
1834            .subchunks
1835            .iter()
1836            .any(|requirement| matches!(requirement, SubchunkDataRequirement::SurfaceColumns(_)))
1837        {
1838            return Err(BedrockWorldError::Validation(
1839                "surface-column loads require a SurfaceColumns data requirement".to_string(),
1840            ));
1841        }
1842        request.subchunks.retain(|requirement| {
1843            !matches!(
1844                requirement,
1845                SubchunkDataRequirement::Layer(_)
1846                    | SubchunkDataRequirement::CaveSlice(_)
1847                    | SubchunkDataRequirement::Full3dIndices
1848            )
1849        });
1850        options.data_request = request;
1851        Ok(self.query_chunk_data_blocking(pos, options)?.column_samples)
1852    }
1853
1854    /// Load render chunks blocking.
1855    pub fn query_chunk_data_many_blocking(
1856        &self,
1857        positions: impl IntoIterator<Item = ChunkPos>,
1858        options: ChunkLoadOptions,
1859    ) -> Result<Vec<ChunkData>> {
1860        Ok(self
1861            .query_chunk_data_with_stats_blocking(positions, options)?
1862            .0)
1863    }
1864
1865    /// Load render chunks with stats blocking.
1866    pub fn query_chunk_data_with_stats_blocking(
1867        &self,
1868        positions: impl IntoIterator<Item = ChunkPos>,
1869        options: ChunkLoadOptions,
1870    ) -> Result<(Vec<ChunkData>, ChunkLoadStats)> {
1871        let started = Instant::now();
1872        let positions = positions.into_iter().collect::<Vec<_>>();
1873        if positions.is_empty() {
1874            log::debug!("loading render chunks skipped (chunks=0)");
1875            return Ok((Vec::new(), ChunkLoadStats::default()));
1876        }
1877        let mut positions = positions;
1878        sort_render_chunk_positions(&mut positions, options.priority);
1879        let worker_count = options.threading.resolve_checked(positions.len())?;
1880        log::debug!(
1881            "loading render chunks (chunks={}, workers={}, data_request={:?}, queue_depth={}, priority={:?})",
1882            positions.len(),
1883            worker_count,
1884            options.data_request,
1885            options
1886                .pipeline
1887                .resolve_queue_depth(worker_count, positions.len()),
1888            options.priority
1889        );
1890        self.load_render_chunks_exact_batch_blocking_sorted(
1891            positions,
1892            options,
1893            worker_count,
1894            started,
1895        )
1896    }
1897
1898    #[allow(clippy::too_many_lines)]
1899    fn load_render_chunks_exact_batch_blocking_sorted(
1900        &self,
1901        positions: Vec<ChunkPos>,
1902        options: ChunkLoadOptions,
1903        worker_count: usize,
1904        started: Instant,
1905    ) -> Result<(Vec<ChunkData>, ChunkLoadStats)> {
1906        check_render_load_cancelled(&options)?;
1907        let mut raw_chunks = positions
1908            .iter()
1909            .copied()
1910            .map(|pos| RawChunkData {
1911                pos,
1912                biome_record: None,
1913                subchunks: BTreeMap::new(),
1914                block_entities: None,
1915                legacy_terrain: None,
1916            })
1917            .collect::<Vec<_>>();
1918
1919        let mut keys = Vec::new();
1920        let mut requests = Vec::new();
1921        for (chunk_index, pos) in positions.iter().copied().enumerate() {
1922            if request_needs_legacy_terrain(&options) {
1923                push_render_record_request(
1924                    &mut keys,
1925                    &mut requests,
1926                    chunk_index,
1927                    pos,
1928                    RenderRecordKind::LegacyTerrain,
1929                );
1930            }
1931            if request_needs_biome_record(&options) {
1932                push_render_record_request(
1933                    &mut keys,
1934                    &mut requests,
1935                    chunk_index,
1936                    pos,
1937                    RenderRecordKind::Data3D,
1938                );
1939                push_render_record_request(
1940                    &mut keys,
1941                    &mut requests,
1942                    chunk_index,
1943                    pos,
1944                    RenderRecordKind::Data2D,
1945                );
1946                push_render_record_request(
1947                    &mut keys,
1948                    &mut requests,
1949                    chunk_index,
1950                    pos,
1951                    RenderRecordKind::Data2DLegacy,
1952                );
1953            }
1954            if !request_uses_hint_surface_subchunks(&options) {
1955                for y in planned_render_subchunk_ys(pos, &options, None)? {
1956                    push_render_record_request(
1957                        &mut keys,
1958                        &mut requests,
1959                        chunk_index,
1960                        pos,
1961                        RenderRecordKind::Subchunk(y),
1962                    );
1963                }
1964            }
1965            if request_loads_block_entities(&options) {
1966                push_render_record_request(
1967                    &mut keys,
1968                    &mut requests,
1969                    chunk_index,
1970                    pos,
1971                    RenderRecordKind::BlockEntity,
1972                );
1973            }
1974        }
1975
1976        let mut keys_requested = keys.len();
1977        let mut exact_get_batches = 0usize;
1978        let mut db_read_ms = 0u128;
1979        let storage_read_options = to_render_storage_read_options(&options);
1980        let db_started = Instant::now();
1981        let values = self
1982            .storage()
1983            .get_many_ordered_with_control(&keys, storage_read_options.clone())?;
1984        db_read_ms = db_read_ms.saturating_add(db_started.elapsed().as_millis());
1985        exact_get_batches = exact_get_batches.saturating_add(usize::from(!keys.is_empty()));
1986        let mut keys_found = apply_render_record_values(&mut raw_chunks, &requests, values);
1987
1988        if request_needs_legacy_terrain_fallback(&options) {
1989            let mut fallback_keys = Vec::new();
1990            let mut fallback_requests = Vec::new();
1991            for (chunk_index, raw) in raw_chunks.iter().enumerate() {
1992                if raw.subchunks.is_empty() && raw.legacy_terrain.is_none() {
1993                    push_render_record_request(
1994                        &mut fallback_keys,
1995                        &mut fallback_requests,
1996                        chunk_index,
1997                        raw.pos,
1998                        RenderRecordKind::LegacyTerrain,
1999                    );
2000                }
2001            }
2002            if !fallback_keys.is_empty() {
2003                let db_started = Instant::now();
2004                let values = self
2005                    .storage()
2006                    .get_many_ordered_with_control(&fallback_keys, storage_read_options.clone())?;
2007                db_read_ms = db_read_ms.saturating_add(db_started.elapsed().as_millis());
2008                exact_get_batches = exact_get_batches.saturating_add(1);
2009                keys_requested = keys_requested.saturating_add(fallback_keys.len());
2010                keys_found = keys_found.saturating_add(apply_render_record_values(
2011                    &mut raw_chunks,
2012                    &fallback_requests,
2013                    values,
2014                ));
2015            }
2016        }
2017
2018        if request_uses_hint_surface_subchunks(&options) {
2019            let mut needed_keys = Vec::new();
2020            let mut needed_requests = Vec::new();
2021            for (chunk_index, raw) in raw_chunks.iter().enumerate() {
2022                let biome_data = parse_render_biome_record(raw.biome_record.as_ref())?;
2023                let height_map = if let Some(biome_data) = biome_data.as_ref() {
2024                    Some(render_height_map_from_biome_data(raw.pos, biome_data))
2025                } else {
2026                    legacy_height_map_from_raw(raw.legacy_terrain.as_ref())?
2027                };
2028                for y in planned_render_subchunk_ys(raw.pos, &options, height_map.as_ref())? {
2029                    if raw.subchunks.contains_key(&y) {
2030                        continue;
2031                    }
2032                    push_render_record_request(
2033                        &mut needed_keys,
2034                        &mut needed_requests,
2035                        chunk_index,
2036                        raw.pos,
2037                        RenderRecordKind::Subchunk(y),
2038                    );
2039                }
2040            }
2041            if !needed_keys.is_empty() {
2042                let db_started = Instant::now();
2043                let values = self
2044                    .storage()
2045                    .get_many_ordered_with_control(&needed_keys, storage_read_options.clone())?;
2046                db_read_ms = db_read_ms.saturating_add(db_started.elapsed().as_millis());
2047                exact_get_batches = exact_get_batches.saturating_add(1);
2048                keys_requested = keys_requested.saturating_add(needed_keys.len());
2049                keys_found = keys_found.saturating_add(apply_render_record_values(
2050                    &mut raw_chunks,
2051                    &needed_requests,
2052                    values,
2053                ));
2054            }
2055        }
2056
2057        check_render_load_cancelled(&options)?;
2058        let decode_started = Instant::now();
2059        let (mut chunks, decode_timing) = if worker_count == 1 {
2060            let mut chunks = Vec::with_capacity(raw_chunks.len());
2061            let mut timing = ChunkDecodeTiming::default();
2062            for raw in raw_chunks {
2063                check_render_load_cancelled(&options)?;
2064                let (chunk, chunk_timing) = render_chunk_from_raw(raw, &options)?;
2065                timing.add(chunk_timing);
2066                chunks.push(chunk);
2067                emit_render_load_progress(&options, chunks.len());
2068            }
2069            (chunks, timing)
2070        } else {
2071            let pool = world_pool(worker_count)?;
2072            let decoded = pool.install(|| {
2073                raw_chunks
2074                    .into_par_iter()
2075                    .map(|raw| {
2076                        check_render_load_cancelled(&options)?;
2077                        render_chunk_from_raw(raw, &options)
2078                    })
2079                    .collect::<Result<Vec<_>>>()
2080            })?;
2081            let mut chunks = Vec::with_capacity(decoded.len());
2082            let mut timing = ChunkDecodeTiming::default();
2083            for (chunk, chunk_timing) in decoded {
2084                timing.add(chunk_timing);
2085                chunks.push(chunk);
2086            }
2087            (chunks, timing)
2088        };
2089        let full_reload_ms =
2090            self.reload_incomplete_needed_exact_surface_chunks_blocking(&mut chunks, &options)?;
2091        let decode_ms = decode_started.elapsed().as_millis();
2092        let mut stats = render_load_stats(&chunks, worker_count, 0, started.elapsed().as_millis());
2093        stats.keys_requested = keys_requested;
2094        stats.keys_found = keys_found;
2095        stats.exact_get_batches = exact_get_batches;
2096        stats.prefix_scans = 0;
2097        stats.decode_ms = decode_ms;
2098        stats.db_read_ms = db_read_ms;
2099        stats.biome_parse_us = decode_timing.biome_parse_us;
2100        stats.subchunk_parse_us = decode_timing.subchunk_parse_us;
2101        stats.surface_scan_us = decode_timing.surface_scan_us;
2102        stats.block_entity_parse_us = decode_timing.block_entity_parse_us;
2103        stats.biome_parse_ms = stats.biome_parse_us / 1_000;
2104        stats.subchunk_parse_ms = stats.subchunk_parse_us / 1_000;
2105        stats.surface_scan_ms = stats.surface_scan_us / 1_000;
2106        stats.block_entity_parse_ms = stats.block_entity_parse_us / 1_000;
2107        stats.full_reload_ms = full_reload_ms;
2108        stats.detected_format = self.format;
2109        stats.legacy_pocket_chunks = if self.format == WorldFormat::PocketChunksDat {
2110            stats.legacy_terrain_records
2111        } else {
2112            0
2113        };
2114        log_render_load_complete(&stats);
2115        Ok((chunks, stats))
2116    }
2117
2118    fn reload_incomplete_needed_exact_surface_chunks_blocking(
2119        &self,
2120        chunks: &mut [ChunkData],
2121        options: &ChunkLoadOptions,
2122    ) -> Result<u128> {
2123        if !request_uses_hint_surface_subchunks(options) {
2124            return Ok(0);
2125        }
2126
2127        let mut full_options = options.clone();
2128        exact_surface_full_request(&mut full_options);
2129        let mut reload_indexes = Vec::new();
2130        let mut reload_positions = Vec::new();
2131        for (index, chunk) in chunks.iter().enumerate() {
2132            if needed_exact_surface_chunk_requires_full_reload(chunk)? {
2133                reload_indexes.push(index);
2134                reload_positions.push(chunk.pos);
2135            }
2136        }
2137        if reload_positions.is_empty() {
2138            return Ok(0);
2139        }
2140        check_render_load_cancelled(options)?;
2141        let started = Instant::now();
2142        let worker_count = options.threading.resolve_checked(reload_positions.len())?;
2143        full_options.threading = if worker_count <= 1 {
2144            WorldThreadingOptions::Single
2145        } else {
2146            WorldThreadingOptions::Fixed(worker_count)
2147        };
2148        let (reloaded, stats) =
2149            self.query_chunk_data_with_stats_blocking(reload_positions, full_options)?;
2150        for (chunk_index, reloaded_chunk) in reload_indexes.into_iter().zip(reloaded) {
2151            if let Some(chunk) = chunks.get_mut(chunk_index) {
2152                *chunk = reloaded_chunk;
2153            }
2154        }
2155        let elapsed = started.elapsed().as_millis().max(stats.load_ms);
2156        log::debug!(
2157            "hint surface full reload complete (chunks={}, workers={}, load_ms={}, db_read_ms={}, decode_ms={})",
2158            stats.requested_chunks,
2159            stats.worker_threads,
2160            stats.load_ms,
2161            stats.db_read_ms,
2162            stats.decode_ms
2163        );
2164        Ok(elapsed)
2165    }
2166
2167    /// Load render region blocking.
2168    pub fn query_chunk_region_blocking(
2169        &self,
2170        region: WorldChunkQueryRegion,
2171        options: WorldChunkQueryRegionLoadOptions,
2172    ) -> Result<WorldChunkQueryRegionData> {
2173        if region.min_chunk_x > region.max_chunk_x || region.min_chunk_z > region.max_chunk_z {
2174            return Err(BedrockWorldError::Validation(format!(
2175                "invalid render region: min=({}, {}) max=({}, {})",
2176                region.min_chunk_x, region.min_chunk_z, region.max_chunk_x, region.max_chunk_z
2177            )));
2178        }
2179        let chunk_count_x = i64::from(region.max_chunk_x) - i64::from(region.min_chunk_x) + 1;
2180        let chunk_count_z = i64::from(region.max_chunk_z) - i64::from(region.min_chunk_z) + 1;
2181        let capacity = usize::try_from(chunk_count_x.saturating_mul(chunk_count_z))
2182            .map_err(|_| BedrockWorldError::Validation("render region is too large".to_string()))?;
2183        let mut positions = Vec::with_capacity(capacity);
2184        for z in region.min_chunk_z..=region.max_chunk_z {
2185            for x in region.min_chunk_x..=region.max_chunk_x {
2186                positions.push(ChunkPos {
2187                    x,
2188                    z,
2189                    dimension: region.dimension,
2190                });
2191            }
2192        }
2193        let (chunks, stats) =
2194            self.query_chunk_data_with_stats_blocking(positions, options.into())?;
2195        Ok(WorldChunkQueryRegionData {
2196            region,
2197            chunks,
2198            stats,
2199        })
2200    }
2201
2202    /// Get block state at blocking.
2203    pub fn get_block_state_at_blocking(
2204        &self,
2205        dimension: crate::Dimension,
2206        block_pos: BlockPos,
2207    ) -> Result<Option<BlockState>> {
2208        let chunk_pos = block_pos.to_chunk_pos(dimension);
2209        let (_, block_y, _) = block_pos.in_chunk_offset();
2210        let subchunk_y = block_y_to_subchunk_y(block_y)?;
2211        let Some(subchunk) = self.parse_subchunk_blocking(
2212            chunk_pos,
2213            subchunk_y,
2214            WorldParseOptions {
2215                subchunk_decode_mode: SubChunkDecodeMode::FullIndices,
2216                ..WorldParseOptions::summary()
2217            },
2218        )?
2219        else {
2220            return Ok(None);
2221        };
2222        let (local_x, _, local_z) = block_pos.in_chunk_offset();
2223        let local_y = u8::try_from(block_y - i32::from(subchunk_y) * 16).map_err(|_| {
2224            BedrockWorldError::Validation(format!("block y={block_y} is outside subchunk bounds"))
2225        })?;
2226        Ok(subchunk.block_state_at(local_x, local_y, local_z).cloned())
2227    }
2228
2229    /// Decodes the subchunk layer containing the requested world Y coordinate.
2230    pub fn get_subchunk_layer_blocking(
2231        &self,
2232        pos: ChunkPos,
2233        y: i32,
2234        mode: SubChunkDecodeMode,
2235    ) -> Result<Option<SubChunk>> {
2236        let subchunk_y = block_y_to_subchunk_y(y)?;
2237        self.parse_subchunk_blocking(
2238            pos,
2239            subchunk_y,
2240            WorldParseOptions {
2241                subchunk_decode_mode: mode,
2242                ..WorldParseOptions::summary()
2243            },
2244        )
2245    }
2246
2247    fn block_state_in_chunk_column(
2248        &self,
2249        pos: ChunkPos,
2250        local_x: u8,
2251        y: i32,
2252        local_z: u8,
2253    ) -> Result<Option<BlockState>> {
2254        let subchunk_y = block_y_to_subchunk_y(y)?;
2255        let Some(subchunk) = self.parse_subchunk_blocking(
2256            pos,
2257            subchunk_y,
2258            WorldParseOptions {
2259                subchunk_decode_mode: SubChunkDecodeMode::FullIndices,
2260                ..WorldParseOptions::summary()
2261            },
2262        )?
2263        else {
2264            return Ok(None);
2265        };
2266        let local_y = u8::try_from(y - i32::from(subchunk_y) * 16).map_err(|_| {
2267            BedrockWorldError::Validation(format!("block y={y} is outside subchunk bounds"))
2268        })?;
2269        Ok(subchunk.block_state_at(local_x, local_y, local_z).cloned())
2270    }
2271
2272    fn find_solid_under_water(
2273        &self,
2274        pos: ChunkPos,
2275        local_x: u8,
2276        local_z: u8,
2277        water_y: i32,
2278        min_y: i32,
2279    ) -> Result<(u8, Option<String>)> {
2280        let mut depth = 0_u8;
2281        for y in (min_y..water_y).rev() {
2282            let Some(block) = self.block_state_in_chunk_column(pos, local_x, y, local_z)? else {
2283                continue;
2284            };
2285            if is_air_block_name(&block.name) || is_water_block_name(&block.name) {
2286                depth = depth.saturating_add(1);
2287                continue;
2288            }
2289            depth = depth.saturating_add(1);
2290            return Ok((depth, Some(block.name)));
2291        }
2292        Ok((depth, None))
2293    }
2294
2295    /// Parse global data blocking.
2296    pub fn parse_global_data_blocking(&self) -> Result<Vec<ParsedDbEntry>> {
2297        parse_global_storage_entries(self.storage(), WorldParseOptions::summary())
2298    }
2299
2300    /// Scan entities blocking.
2301    pub fn scan_entities_blocking(
2302        &self,
2303        options: WorldScanOptions,
2304    ) -> Result<(Vec<ParsedEntity>, WorldParseReport)> {
2305        let mut report = WorldParseReport::default();
2306        let mut entities = Vec::new();
2307        let mut entries_seen = 0usize;
2308        self.storage()
2309            .for_each_entry(to_storage_read_options(&options), &mut |key, value| {
2310                check_cancelled(&options)?;
2311                entries_seen = entries_seen.saturating_add(1);
2312                match BedrockDbKey::decode(key) {
2313                    BedrockDbKey::ActorPrefix { .. } => {
2314                        entities.extend(parse_entities_from_value(value, &mut report));
2315                    }
2316                    BedrockDbKey::Chunk(chunk_key) if chunk_key.tag == ChunkRecordTag::Entity => {
2317                        entities.extend(parse_entities_from_value(value, &mut report));
2318                    }
2319                    _ => {}
2320                }
2321                if entries_seen.is_multiple_of(8192) {
2322                    emit_progress(&options, entries_seen);
2323                }
2324                Ok(StorageVisitorControl::Continue)
2325            })?;
2326        Ok((entities, report))
2327    }
2328
2329    /// Scan block entities blocking.
2330    pub fn scan_block_entities_blocking(
2331        &self,
2332        options: WorldScanOptions,
2333    ) -> Result<(Vec<ParsedBlockEntity>, WorldParseReport)> {
2334        let mut report = WorldParseReport::default();
2335        let mut block_entities = Vec::new();
2336        let mut entries_seen = 0usize;
2337        self.storage()
2338            .for_each_entry(to_storage_read_options(&options), &mut |key, value| {
2339                check_cancelled(&options)?;
2340                entries_seen = entries_seen.saturating_add(1);
2341                if let BedrockDbKey::Chunk(chunk_key) = BedrockDbKey::decode(key) {
2342                    if chunk_key.tag == ChunkRecordTag::BlockEntity {
2343                        block_entities.extend(parse_block_entities_from_value(value, &mut report));
2344                    }
2345                }
2346                if entries_seen.is_multiple_of(8192) {
2347                    emit_progress(&options, entries_seen);
2348                }
2349                Ok(StorageVisitorControl::Continue)
2350            })?;
2351        Ok((block_entities, report))
2352    }
2353
2354    /// Scan items blocking.
2355    pub fn scan_items_blocking(
2356        &self,
2357        options: WorldScanOptions,
2358    ) -> Result<(Vec<ItemStack>, WorldParseReport)> {
2359        let mut report = WorldParseReport::default();
2360        let mut items = Vec::new();
2361        let mut entries_seen = 0usize;
2362        self.storage()
2363            .for_each_entry(to_storage_read_options(&options), &mut |key, value| {
2364                check_cancelled(&options)?;
2365                entries_seen = entries_seen.saturating_add(1);
2366                match BedrockDbKey::decode(key) {
2367                    BedrockDbKey::LocalPlayer | BedrockDbKey::RemotePlayer(_) => {
2368                        match parse_root_nbt(value) {
2369                            Ok(nbt) => {
2370                                let mut player_items = collect_item_stacks(&nbt);
2371                                report.item_count =
2372                                    report.item_count.saturating_add(player_items.len());
2373                                items.append(&mut player_items);
2374                            }
2375                            Err(error) => report
2376                                .parse_errors
2377                                .push(format!("player item scan failed: {error}")),
2378                        }
2379                    }
2380                    BedrockDbKey::ActorPrefix { .. } => {
2381                        for entity in parse_entities_from_value(value, &mut report) {
2382                            items.extend(entity.items);
2383                        }
2384                    }
2385                    BedrockDbKey::Chunk(chunk_key) if chunk_key.tag == ChunkRecordTag::Entity => {
2386                        for entity in parse_entities_from_value(value, &mut report) {
2387                            items.extend(entity.items);
2388                        }
2389                    }
2390                    BedrockDbKey::Chunk(chunk_key)
2391                        if chunk_key.tag == ChunkRecordTag::BlockEntity =>
2392                    {
2393                        for block_entity in parse_block_entities_from_value(value, &mut report) {
2394                            items.extend(block_entity.items);
2395                        }
2396                    }
2397                    _ => {}
2398                }
2399                if entries_seen.is_multiple_of(8192) {
2400                    emit_progress(&options, entries_seen);
2401                }
2402                Ok(StorageVisitorControl::Continue)
2403            })?;
2404        Ok((items, report))
2405    }
2406
2407    /// Scans map records through the full global-data parser.
2408    ///
2409    /// Prefer [`Self::scan_map_records_blocking`] when only `map_` records are
2410    /// needed because it uses an exact prefix scan.
2411    ///
2412    /// # Errors
2413    ///
2414    /// Returns storage or parse errors from the underlying world scan.
2415    pub fn scan_maps_blocking(&self) -> Result<Vec<ParsedMapData>> {
2416        Ok(self
2417            .parse_global_data_blocking()?
2418            .into_iter()
2419            .filter_map(|entry| match entry.value {
2420                ParsedDbValue::MapData(value) => Some(value),
2421                _ => None,
2422            })
2423            .collect())
2424    }
2425
2426    /// Reads a single typed map record by exact `map_<id>` key.
2427    ///
2428    /// # Errors
2429    ///
2430    /// Returns storage errors or map NBT parse errors.
2431    pub fn read_map_record_blocking(&self, id: &MapRecordId) -> Result<Option<ParsedMapData>> {
2432        self.storage()
2433            .get(&id.storage_key())?
2434            .map(|value| parse_map_record(id.clone(), value))
2435            .transpose()
2436    }
2437
2438    /// Prefix-scans typed map records without scanning unrelated globals.
2439    ///
2440    /// # Errors
2441    ///
2442    /// Returns storage errors, cancellation, or map NBT parse errors.
2443    pub fn scan_map_records_blocking(
2444        &self,
2445        options: WorldScanOptions,
2446    ) -> Result<Vec<ParsedMapData>> {
2447        let mut records = Vec::new();
2448        self.storage().for_each_prefix_ref(
2449            b"map_",
2450            to_storage_read_options(&options),
2451            &mut |entry| {
2452                check_cancelled(&options)?;
2453                let Some(id) = MapRecordId::from_storage_key(entry.key) else {
2454                    return Ok(StorageVisitorControl::Continue);
2455                };
2456                records.push(parse_map_record(id, Bytes::copy_from_slice(entry.value))?);
2457                Ok(StorageVisitorControl::Continue)
2458            },
2459        )?;
2460        Ok(records)
2461    }
2462
2463    /// Writes a map record after serialize -> parse roundtrip validation.
2464    ///
2465    /// # Errors
2466    ///
2467    /// Returns [`BedrockWorldError::ReadOnly`] for read-only worlds, validation
2468    /// errors for malformed records, or storage errors from the commit.
2469    pub fn write_map_record_blocking(&self, record: &ParsedMapData) -> Result<()> {
2470        self.ensure_writable()?;
2471        let value = encode_map_record(record)?;
2472        parse_map_record(record.record_id.clone(), value.clone())?;
2473        let mut transaction = self.transaction();
2474        transaction.put_raw_key(record.record_id.storage_key(), value);
2475        transaction.commit()
2476    }
2477
2478    /// Deletes a map record by exact id.
2479    ///
2480    /// # Errors
2481    ///
2482    /// Returns [`BedrockWorldError::ReadOnly`] for read-only worlds or storage
2483    /// errors from the commit.
2484    pub fn delete_map_record_blocking(&self, id: &MapRecordId) -> Result<()> {
2485        self.ensure_writable()?;
2486        let mut transaction = self.transaction();
2487        transaction.delete_raw_key(id.storage_key());
2488        transaction.commit()
2489    }
2490
2491    /// Scans village records through the full global-data parser.
2492    ///
2493    /// # Errors
2494    ///
2495    /// Returns storage or parse errors from the underlying world scan.
2496    pub fn scan_villages_blocking(&self) -> Result<Vec<ParsedVillageData>> {
2497        Ok(self
2498            .parse_global_data_blocking()?
2499            .into_iter()
2500            .filter_map(|entry| match entry.value {
2501                ParsedDbValue::VillageData(value) => Some(value),
2502                _ => None,
2503            })
2504            .collect())
2505    }
2506
2507    /// Scan villages lightweight blocking.
2508    pub fn scan_villages_lightweight_blocking(
2509        &self,
2510        cancel: &CancelFlag,
2511    ) -> Result<Vec<ParsedVillageData>> {
2512        let mut villages = Vec::new();
2513        let options = StorageReadOptions {
2514            cancel: Some(cancel.to_storage_cancel()),
2515            ..StorageReadOptions::default()
2516        };
2517        self.storage()
2518            .for_each_prefix_ref(b"VILLAGE_", options, &mut |entry| {
2519                if cancel.is_cancelled() {
2520                    return Err(BedrockWorldError::Cancelled {
2521                        operation: "village scan",
2522                    });
2523                }
2524                let BedrockDbKey::Village(key) = BedrockDbKey::decode(entry.key) else {
2525                    return Ok(StorageVisitorControl::Continue);
2526                };
2527                let roots = parse_consecutive_root_nbt(entry.value).unwrap_or_default();
2528                villages.push(ParsedVillageData {
2529                    key,
2530                    roots,
2531                    raw: Bytes::new(),
2532                });
2533                Ok(StorageVisitorControl::Continue)
2534            })?;
2535        Ok(villages)
2536    }
2537
2538    /// Scans global records through the full global-data parser.
2539    ///
2540    /// Prefer [`Self::scan_global_records_blocking`] when only typed global
2541    /// records are needed.
2542    ///
2543    /// # Errors
2544    ///
2545    /// Returns storage or parse errors from the underlying world scan.
2546    pub fn scan_globals_blocking(&self) -> Result<Vec<ParsedGlobalData>> {
2547        Ok(self
2548            .parse_global_data_blocking()?
2549            .into_iter()
2550            .filter_map(|entry| match entry.value {
2551                ParsedDbValue::GlobalData(value) => Some(value),
2552                _ => None,
2553            })
2554            .collect())
2555    }
2556
2557    /// Reads a single typed global record by exact key.
2558    ///
2559    /// # Errors
2560    ///
2561    /// Returns storage errors or global NBT parse errors.
2562    pub fn read_global_record_blocking(
2563        &self,
2564        kind: GlobalRecordKind,
2565    ) -> Result<Option<ParsedGlobalData>> {
2566        let key = kind.storage_key();
2567        self.storage()
2568            .get(&key)?
2569            .map(|value| parse_global_record(kind.clone(), kind.name(), value))
2570            .transpose()
2571    }
2572
2573    /// Scans known global records while preserving each typed key kind.
2574    ///
2575    /// # Errors
2576    ///
2577    /// Returns storage errors, cancellation, or global NBT parse errors.
2578    pub fn scan_global_records_blocking(
2579        &self,
2580        options: WorldScanOptions,
2581    ) -> Result<Vec<ParsedGlobalData>> {
2582        let mut records = Vec::new();
2583        self.storage()
2584            .for_each_entry(to_storage_read_options(&options), &mut |key, value| {
2585                check_cancelled(&options)?;
2586                let BedrockDbKey::Global(kind) = BedrockDbKey::decode(key) else {
2587                    return Ok(StorageVisitorControl::Continue);
2588                };
2589                records.push(parse_global_record(
2590                    kind.clone(),
2591                    kind.name(),
2592                    value.clone(),
2593                )?);
2594                Ok(StorageVisitorControl::Continue)
2595            })?;
2596        Ok(records)
2597    }
2598
2599    /// Writes a global record after serialize -> parse roundtrip validation.
2600    ///
2601    /// # Errors
2602    ///
2603    /// Returns [`BedrockWorldError::ReadOnly`] for read-only worlds, validation
2604    /// errors for malformed records, or storage errors from the commit.
2605    pub fn write_global_record_blocking(&self, record: &ParsedGlobalData) -> Result<()> {
2606        self.ensure_writable()?;
2607        let value = encode_global_record(record)?;
2608        parse_global_record(record.kind.clone(), record.name.clone(), value.clone())?;
2609        let mut transaction = self.transaction();
2610        transaction.put_raw_key(record.kind.storage_key(), value);
2611        transaction.commit()
2612    }
2613
2614    /// Deletes a typed global record.
2615    ///
2616    /// # Errors
2617    ///
2618    /// Returns [`BedrockWorldError::ReadOnly`] for read-only worlds or storage
2619    /// errors from the commit.
2620    pub fn delete_global_record_blocking(&self, kind: GlobalRecordKind) -> Result<()> {
2621        self.ensure_writable()?;
2622        let mut transaction = self.transaction();
2623        transaction.delete_raw_key(kind.storage_key());
2624        transaction.commit()
2625    }
2626
2627    /// Reads the Data2D/Data3D height map for a chunk.
2628    ///
2629    /// # Errors
2630    ///
2631    /// Returns storage errors or biome/heightmap parse errors.
2632    pub fn get_heightmap_blocking(&self, pos: ChunkPos) -> Result<Option<HeightMap2d>> {
2633        self.get_biome_data_blocking(pos)?
2634            .map(|data| HeightMap2d::new(data.height_map))
2635            .transpose()
2636    }
2637
2638    /// Writes a chunk height map while preserving existing `Data3D` biome storages.
2639    ///
2640    /// # Errors
2641    ///
2642    /// Returns [`BedrockWorldError::ReadOnly`] for read-only worlds, validation
2643    /// errors for invalid height map length, or storage errors.
2644    pub fn put_heightmap_blocking(
2645        &self,
2646        pos: ChunkPos,
2647        version: ChunkVersion,
2648        height_map: HeightMap2d,
2649    ) -> Result<()> {
2650        self.ensure_writable()?;
2651        let existing = self.get_biome_data_blocking(pos)?;
2652        let storages = existing.map_or_else(Vec::new, |data| data.storages);
2653        let value = match version {
2654            ChunkVersion::Old => Biome2d::new(height_map.values, vec![0; 256])?.encode()?,
2655            ChunkVersion::New => Biome3d::new(height_map.values, storages)?.encode()?,
2656        };
2657        let tag = match version {
2658            ChunkVersion::Old => ChunkRecordTag::Data2D,
2659            ChunkVersion::New => ChunkRecordTag::Data3D,
2660        };
2661        self.put_raw_record_blocking(&ChunkKey::new(pos, tag), &value)
2662    }
2663
2664    /// Writes a full `Data3D` biome payload after roundtrip validation.
2665    ///
2666    /// # Errors
2667    ///
2668    /// Returns [`BedrockWorldError::ReadOnly`] for read-only worlds, validation
2669    /// errors for malformed biome storage, or storage errors.
2670    pub fn put_biome_storage_blocking(&self, pos: ChunkPos, biome: Biome3d) -> Result<()> {
2671        self.ensure_writable()?;
2672        let value = biome.encode()?;
2673        Biome3d::parse(&value)?;
2674        self.put_raw_record_blocking(&ChunkKey::new(pos, ChunkRecordTag::Data3D), &value)
2675    }
2676
2677    /// Scans hardcoded spawn area records across the world.
2678    ///
2679    /// # Errors
2680    ///
2681    /// Returns storage errors, cancellation, or HSA payload validation errors.
2682    pub fn scan_hsa_records_blocking(
2683        &self,
2684        options: WorldScanOptions,
2685    ) -> Result<Vec<(ChunkPos, Vec<ParsedHardcodedSpawnArea>)>> {
2686        let mut records = Vec::new();
2687        self.storage()
2688            .for_each_entry(to_storage_read_options(&options), &mut |key, value| {
2689                check_cancelled(&options)?;
2690                let BedrockDbKey::Chunk(chunk_key) = BedrockDbKey::decode(key) else {
2691                    return Ok(StorageVisitorControl::Continue);
2692                };
2693                if chunk_key.tag == ChunkRecordTag::HardcodedSpawners {
2694                    records.push((chunk_key.pos, parse_hardcoded_spawn_area_records(value)?));
2695                }
2696                Ok(StorageVisitorControl::Continue)
2697            })?;
2698        Ok(records)
2699    }
2700
2701    /// Writes hardcoded spawn areas for one chunk.
2702    ///
2703    /// # Errors
2704    ///
2705    /// Returns [`BedrockWorldError::ReadOnly`] for read-only worlds, validation
2706    /// errors for invalid bounds/lengths, or storage errors.
2707    pub fn put_hsa_for_chunk_blocking(
2708        &self,
2709        pos: ChunkPos,
2710        areas: &[ParsedHardcodedSpawnArea],
2711    ) -> Result<()> {
2712        self.ensure_writable()?;
2713        let value = encode_hardcoded_spawn_area_records(areas)?;
2714        parse_hardcoded_spawn_area_records(&value)?;
2715        let mut transaction = self.transaction();
2716        transaction.put_raw_record(
2717            &ChunkKey::new(pos, ChunkRecordTag::HardcodedSpawners),
2718            value,
2719        );
2720        transaction.commit()
2721    }
2722
2723    /// Deletes hardcoded spawn areas for one chunk.
2724    ///
2725    /// # Errors
2726    ///
2727    /// Returns [`BedrockWorldError::ReadOnly`] for read-only worlds or storage
2728    /// errors.
2729    pub fn delete_hsa_for_chunk_blocking(&self, pos: ChunkPos) -> Result<()> {
2730        self.delete_raw_record_blocking(&ChunkKey::new(pos, ChunkRecordTag::HardcodedSpawners))
2731    }
2732
2733    /// Reads all block entities from a chunk's consecutive NBT payload.
2734    ///
2735    /// # Errors
2736    ///
2737    /// Returns storage errors or block-entity NBT parse errors.
2738    pub fn block_entities_in_chunk_blocking(
2739        &self,
2740        pos: ChunkPos,
2741    ) -> Result<Vec<BlockEntityRecord>> {
2742        let key = ChunkKey::new(pos, ChunkRecordTag::BlockEntity).encode();
2743        let Some(value) = self.storage().get(&key)? else {
2744            return Ok(Vec::new());
2745        };
2746        let mut report = WorldParseReport::default();
2747        Ok(parse_block_entities_from_value(&value, &mut report)
2748            .into_iter()
2749            .enumerate()
2750            .map(|(index, entity)| BlockEntityRecord {
2751                chunk: pos,
2752                index,
2753                entity,
2754            })
2755            .collect())
2756    }
2757
2758    /// Replaces a chunk's block entity payload after coordinate validation.
2759    ///
2760    /// # Errors
2761    ///
2762    /// Returns [`BedrockWorldError::ReadOnly`] for read-only worlds, validation
2763    /// errors when entity coordinates do not belong to `pos`, or storage errors.
2764    pub fn put_block_entities_blocking(
2765        &self,
2766        pos: ChunkPos,
2767        entities: &[ParsedBlockEntity],
2768    ) -> Result<()> {
2769        self.ensure_writable()?;
2770        validate_block_entities_in_chunk(pos, entities)?;
2771        let roots = entities
2772            .iter()
2773            .map(|entity| entity.nbt.clone())
2774            .collect::<Vec<_>>();
2775        let value = encode_consecutive_roots(&roots)?;
2776        let mut report = WorldParseReport::default();
2777        let parsed = parse_block_entities_from_value(&value, &mut report);
2778        validate_block_entities_in_chunk(pos, &parsed)?;
2779        let mut transaction = self.transaction();
2780        transaction.put_raw_record(&ChunkKey::new(pos, ChunkRecordTag::BlockEntity), value);
2781        transaction.commit()
2782    }
2783
2784    /// Edits one block entity in place and rewrites the chunk payload.
2785    ///
2786    /// # Errors
2787    ///
2788    /// Returns validation errors when no block entity exists at `block`, when
2789    /// the edited NBT no longer parses as a block entity, or storage/read-only
2790    /// errors from the write.
2791    pub fn edit_block_entity_at_blocking<F>(
2792        &self,
2793        pos: ChunkPos,
2794        block: BlockPos,
2795        edit: F,
2796    ) -> Result<()>
2797    where
2798        F: FnOnce(&mut NbtTag) -> Result<()>,
2799    {
2800        self.ensure_writable()?;
2801        let mut entities = self
2802            .block_entities_in_chunk_blocking(pos)?
2803            .into_iter()
2804            .map(|record| record.entity)
2805            .collect::<Vec<_>>();
2806        let Some(index) = entities
2807            .iter()
2808            .position(|entity| entity.position == Some([block.x, block.y, block.z]))
2809        else {
2810            return Err(BedrockWorldError::Validation(format!(
2811                "no block entity exists at {},{},{}",
2812                block.x, block.y, block.z
2813            )));
2814        };
2815        edit(&mut entities[index].nbt)?;
2816        let mut report = WorldParseReport::default();
2817        entities[index] = parse_block_entities_from_value(
2818            &Bytes::from(serialize_root_nbt(&entities[index].nbt)?),
2819            &mut report,
2820        )
2821        .into_iter()
2822        .next()
2823        .ok_or_else(|| BedrockWorldError::Validation("edited block entity vanished".to_string()))?;
2824        self.put_block_entities_blocking(pos, &entities)
2825    }
2826
2827    /// Deletes one block entity by absolute block position.
2828    ///
2829    /// # Errors
2830    ///
2831    /// Returns [`BedrockWorldError::ReadOnly`] for read-only worlds or storage
2832    /// errors from rewriting/deleting the payload.
2833    pub fn delete_block_entity_at_blocking(&self, pos: ChunkPos, block: BlockPos) -> Result<()> {
2834        self.ensure_writable()?;
2835        let entities = self
2836            .block_entities_in_chunk_blocking(pos)?
2837            .into_iter()
2838            .map(|record| record.entity)
2839            .filter(|entity| entity.position != Some([block.x, block.y, block.z]))
2840            .collect::<Vec<_>>();
2841        if entities.is_empty() {
2842            return self
2843                .delete_raw_record_blocking(&ChunkKey::new(pos, ChunkRecordTag::BlockEntity));
2844        }
2845        self.put_block_entities_blocking(pos, &entities)
2846    }
2847
2848    /// Reads actors from both legacy inline `Entity` and modern digest/prefix storage.
2849    ///
2850    /// # Errors
2851    ///
2852    /// Returns storage errors or digest validation errors.
2853    pub fn actors_in_chunk_blocking(&self, pos: ChunkPos) -> Result<Vec<ActorRecord>> {
2854        let mut records = Vec::new();
2855        let inline_key = ChunkKey::new(pos, ChunkRecordTag::Entity);
2856        if let Some(value) = self.storage().get(&inline_key.encode())? {
2857            let mut report = WorldParseReport::default();
2858            records.extend(
2859                parse_entities_from_value(&value, &mut report)
2860                    .into_iter()
2861                    .map(|entity| ActorRecord {
2862                        uid: entity.unique_id.map(ActorUid),
2863                        source: ActorSource::InlineChunk(inline_key.clone()),
2864                        entity,
2865                        raw: value.clone(),
2866                    }),
2867            );
2868        }
2869        let digest_key = ActorDigestKey::new(pos).storage_key();
2870        let Some(digest) = self.storage().get(&digest_key)? else {
2871            return Ok(records);
2872        };
2873        let ids = parse_actor_digest_ids(&digest)?;
2874        let actor_keys = ids.iter().map(|id| id.storage_key()).collect::<Vec<_>>();
2875        let values = self.storage().get_many(&actor_keys)?;
2876        for (id, value) in ids.into_iter().zip(values) {
2877            let Some(value) = value else {
2878                continue;
2879            };
2880            let mut report = WorldParseReport::default();
2881            records.extend(
2882                parse_entities_from_value(&value, &mut report)
2883                    .into_iter()
2884                    .map(|entity| ActorRecord {
2885                        uid: Some(id),
2886                        source: ActorSource::ActorPrefix(id),
2887                        entity,
2888                        raw: value.clone(),
2889                    }),
2890            );
2891        }
2892        Ok(records)
2893    }
2894
2895    /// Writes a modern actor record and updates the chunk actor digest.
2896    ///
2897    /// # Errors
2898    ///
2899    /// Returns [`BedrockWorldError::ReadOnly`] for read-only worlds, validation
2900    /// errors when `actor` has no `UniqueID`, or storage errors from the commit.
2901    pub fn put_actor_blocking(&self, pos: ChunkPos, actor: &ParsedEntity) -> Result<()> {
2902        self.ensure_writable()?;
2903        let uid = actor.unique_id.map(ActorUid).ok_or_else(|| {
2904            BedrockWorldError::Validation("actor UniqueID is required".to_string())
2905        })?;
2906        let value = Bytes::from(serialize_root_nbt(&actor.nbt)?);
2907        parse_entities_from_value(&value, &mut WorldParseReport::default());
2908        let mut transaction = self.transaction();
2909        transaction.put_actor(pos, uid, value)?;
2910        transaction.commit()
2911    }
2912
2913    /// Deletes a modern actor record and removes it from the chunk digest.
2914    ///
2915    /// # Errors
2916    ///
2917    /// Returns [`BedrockWorldError::ReadOnly`] for read-only worlds or storage
2918    /// errors from the commit.
2919    pub fn delete_actor_blocking(&self, pos: ChunkPos, uid: ActorUid) -> Result<()> {
2920        self.ensure_writable()?;
2921        let mut transaction = self.transaction();
2922        transaction.delete_actor(pos, uid)?;
2923        transaction.commit()
2924    }
2925
2926    /// Moves a modern actor between chunk digests and rewrites its actorprefix payload.
2927    ///
2928    /// # Errors
2929    ///
2930    /// Returns [`BedrockWorldError::ReadOnly`] for read-only worlds, validation
2931    /// errors when `actor` has no `UniqueID`, or storage errors from the commit.
2932    pub fn move_actor_blocking(
2933        &self,
2934        from: ChunkPos,
2935        to: ChunkPos,
2936        actor: &ParsedEntity,
2937    ) -> Result<()> {
2938        self.ensure_writable()?;
2939        let uid = actor.unique_id.map(ActorUid).ok_or_else(|| {
2940            BedrockWorldError::Validation("actor UniqueID is required".to_string())
2941        })?;
2942        let value = Bytes::from(serialize_root_nbt(&actor.nbt)?);
2943        let mut transaction = self.transaction();
2944        transaction.delete_actor(from, uid)?;
2945        transaction.put_actor(to, uid, value)?;
2946        transaction.commit()
2947    }
2948
2949    #[cfg(feature = "async")]
2950    /// List players.
2951    pub async fn list_players(&self) -> Result<Vec<PlayerId>> {
2952        let world = self.blocking_clone();
2953        tokio::task::spawn_blocking(move || world.list_players_blocking())
2954            .await
2955            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
2956    }
2957
2958    #[cfg(feature = "async")]
2959    /// Classify keys.
2960    pub async fn classify_keys(
2961        &self,
2962        options: WorldScanOptions,
2963    ) -> Result<BTreeMap<String, usize>> {
2964        let world = self.blocking_clone();
2965        tokio::task::spawn_blocking(move || world.classify_keys_blocking(options))
2966            .await
2967            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
2968    }
2969
2970    #[cfg(feature = "async")]
2971    /// List chunk positions.
2972    pub async fn list_chunk_positions(&self, options: WorldScanOptions) -> Result<Vec<ChunkPos>> {
2973        let world = self.blocking_clone();
2974        tokio::task::spawn_blocking(move || world.list_chunk_positions_blocking(options))
2975            .await
2976            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
2977    }
2978
2979    #[cfg(feature = "async")]
2980    /// List render chunk positions.
2981    pub async fn list_render_chunk_positions(
2982        &self,
2983        options: WorldScanOptions,
2984    ) -> Result<Vec<ChunkPos>> {
2985        let world = self.blocking_clone();
2986        tokio::task::spawn_blocking(move || world.list_render_chunk_positions_blocking(options))
2987            .await
2988            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
2989    }
2990
2991    #[cfg(feature = "async")]
2992    /// List render chunk positions in region.
2993    pub async fn list_render_chunk_positions_in_region(
2994        &self,
2995        region: WorldChunkQueryRegion,
2996        options: WorldScanOptions,
2997    ) -> Result<Vec<ChunkPos>> {
2998        let world = self.blocking_clone();
2999        tokio::task::spawn_blocking(move || {
3000            world.list_chunk_positions_in_region_blocking(region, options)
3001        })
3002        .await
3003        .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3004    }
3005
3006    #[cfg(feature = "async")]
3007    /// Discover chunk bounds.
3008    pub async fn discover_chunk_bounds(
3009        &self,
3010        dimension: crate::Dimension,
3011        options: WorldScanOptions,
3012    ) -> Result<Option<ChunkBounds>> {
3013        let world = self.blocking_clone();
3014        tokio::task::spawn_blocking(move || {
3015            world.discover_chunk_bounds_blocking(dimension, options)
3016        })
3017        .await
3018        .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3019    }
3020
3021    #[cfg(feature = "async")]
3022    /// Nearest loaded chunk to spawn.
3023    pub async fn nearest_loaded_chunk_to_spawn(
3024        &self,
3025        dimension: crate::Dimension,
3026        spawn_block_x: i32,
3027        spawn_block_z: i32,
3028        options: WorldScanOptions,
3029    ) -> Result<Option<ChunkPos>> {
3030        let world = self.blocking_clone();
3031        tokio::task::spawn_blocking(move || {
3032            world.nearest_loaded_chunk_to_spawn_blocking(
3033                dimension,
3034                spawn_block_x,
3035                spawn_block_z,
3036                options,
3037            )
3038        })
3039        .await
3040        .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3041    }
3042
3043    #[cfg(feature = "async")]
3044    /// Parse chunk.
3045    pub async fn parse_chunk(
3046        &self,
3047        pos: ChunkPos,
3048        options: WorldParseOptions,
3049    ) -> Result<ParsedChunkData> {
3050        let world = self.blocking_clone();
3051        tokio::task::spawn_blocking(move || world.parse_chunk_with_options_blocking(pos, options))
3052            .await
3053            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3054    }
3055
3056    #[cfg(feature = "async")]
3057    /// Load render chunk.
3058    pub async fn load_render_chunk(
3059        &self,
3060        pos: ChunkPos,
3061        options: ChunkLoadOptions,
3062    ) -> Result<ChunkData> {
3063        let world = self.blocking_clone();
3064        tokio::task::spawn_blocking(move || world.query_chunk_data_blocking(pos, options))
3065            .await
3066            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3067    }
3068
3069    #[cfg(feature = "async")]
3070    /// Load render chunks.
3071    pub async fn load_render_chunks(
3072        &self,
3073        positions: Vec<ChunkPos>,
3074        options: ChunkLoadOptions,
3075    ) -> Result<Vec<ChunkData>> {
3076        let world = self.blocking_clone();
3077        tokio::task::spawn_blocking(move || {
3078            world.query_chunk_data_many_blocking(positions, options)
3079        })
3080        .await
3081        .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3082    }
3083
3084    #[cfg(feature = "async")]
3085    /// Load render region.
3086    pub async fn load_render_region(
3087        &self,
3088        region: WorldChunkQueryRegion,
3089        options: WorldChunkQueryRegionLoadOptions,
3090    ) -> Result<WorldChunkQueryRegionData> {
3091        let world = self.blocking_clone();
3092        tokio::task::spawn_blocking(move || world.query_chunk_region_blocking(region, options))
3093            .await
3094            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3095    }
3096
3097    #[cfg(feature = "async")]
3098    /// Scan entities.
3099    pub async fn scan_entities(
3100        &self,
3101        options: WorldScanOptions,
3102    ) -> Result<(Vec<ParsedEntity>, WorldParseReport)> {
3103        let world = self.blocking_clone();
3104        tokio::task::spawn_blocking(move || world.scan_entities_blocking(options))
3105            .await
3106            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3107    }
3108
3109    #[cfg(feature = "async")]
3110    /// Scan block entities.
3111    pub async fn scan_block_entities(
3112        &self,
3113        options: WorldScanOptions,
3114    ) -> Result<(Vec<ParsedBlockEntity>, WorldParseReport)> {
3115        let world = self.blocking_clone();
3116        tokio::task::spawn_blocking(move || world.scan_block_entities_blocking(options))
3117            .await
3118            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3119    }
3120
3121    #[cfg(feature = "async")]
3122    /// Scan items.
3123    pub async fn scan_items(
3124        &self,
3125        options: WorldScanOptions,
3126    ) -> Result<(Vec<ItemStack>, WorldParseReport)> {
3127        let world = self.blocking_clone();
3128        tokio::task::spawn_blocking(move || world.scan_items_blocking(options))
3129            .await
3130            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3131    }
3132
3133    #[cfg(feature = "async")]
3134    /// Scan maps.
3135    pub async fn scan_maps(&self) -> Result<Vec<ParsedMapData>> {
3136        let world = self.blocking_clone();
3137        tokio::task::spawn_blocking(move || world.scan_maps_blocking())
3138            .await
3139            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3140    }
3141
3142    #[cfg(feature = "async")]
3143    /// Scan villages.
3144    pub async fn scan_villages(&self) -> Result<Vec<ParsedVillageData>> {
3145        let world = self.blocking_clone();
3146        tokio::task::spawn_blocking(move || world.scan_villages_blocking())
3147            .await
3148            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3149    }
3150
3151    #[cfg(feature = "async")]
3152    /// Scan globals.
3153    pub async fn scan_globals(&self) -> Result<Vec<ParsedGlobalData>> {
3154        let world = self.blocking_clone();
3155        tokio::task::spawn_blocking(move || world.scan_globals_blocking())
3156            .await
3157            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3158    }
3159
3160    /// Async wrapper for [`Self::read_map_record_blocking`].
3161    ///
3162    /// # Errors
3163    ///
3164    /// Returns join, storage, or map parse errors.
3165    #[cfg(feature = "async")]
3166    pub async fn read_map_record(&self, id: MapRecordId) -> Result<Option<ParsedMapData>> {
3167        let world = self.blocking_clone();
3168        tokio::task::spawn_blocking(move || world.read_map_record_blocking(&id))
3169            .await
3170            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3171    }
3172
3173    /// Async wrapper for [`Self::scan_map_records_blocking`].
3174    ///
3175    /// # Errors
3176    ///
3177    /// Returns join, storage, cancellation, or map parse errors.
3178    #[cfg(feature = "async")]
3179    pub async fn scan_map_records(&self, options: WorldScanOptions) -> Result<Vec<ParsedMapData>> {
3180        let world = self.blocking_clone();
3181        tokio::task::spawn_blocking(move || world.scan_map_records_blocking(options))
3182            .await
3183            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3184    }
3185
3186    /// Async wrapper for [`Self::write_map_record_blocking`].
3187    ///
3188    /// # Errors
3189    ///
3190    /// Returns join, read-only, validation, or storage errors.
3191    #[cfg(feature = "async")]
3192    pub async fn write_map_record(&self, record: ParsedMapData) -> Result<()> {
3193        let world = self.blocking_clone();
3194        tokio::task::spawn_blocking(move || world.write_map_record_blocking(&record))
3195            .await
3196            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3197    }
3198
3199    /// Async wrapper for [`Self::delete_map_record_blocking`].
3200    ///
3201    /// # Errors
3202    ///
3203    /// Returns join, read-only, or storage errors.
3204    #[cfg(feature = "async")]
3205    pub async fn delete_map_record(&self, id: MapRecordId) -> Result<()> {
3206        let world = self.blocking_clone();
3207        tokio::task::spawn_blocking(move || world.delete_map_record_blocking(&id))
3208            .await
3209            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3210    }
3211
3212    /// Async wrapper for [`Self::read_global_record_blocking`].
3213    ///
3214    /// # Errors
3215    ///
3216    /// Returns join, storage, or global parse errors.
3217    #[cfg(feature = "async")]
3218    pub async fn read_global_record(
3219        &self,
3220        kind: GlobalRecordKind,
3221    ) -> Result<Option<ParsedGlobalData>> {
3222        let world = self.blocking_clone();
3223        tokio::task::spawn_blocking(move || world.read_global_record_blocking(kind))
3224            .await
3225            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3226    }
3227
3228    /// Async wrapper for [`Self::scan_global_records_blocking`].
3229    ///
3230    /// # Errors
3231    ///
3232    /// Returns join, storage, cancellation, or global parse errors.
3233    #[cfg(feature = "async")]
3234    pub async fn scan_global_records(
3235        &self,
3236        options: WorldScanOptions,
3237    ) -> Result<Vec<ParsedGlobalData>> {
3238        let world = self.blocking_clone();
3239        tokio::task::spawn_blocking(move || world.scan_global_records_blocking(options))
3240            .await
3241            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3242    }
3243
3244    /// Async wrapper for [`Self::write_global_record_blocking`].
3245    ///
3246    /// # Errors
3247    ///
3248    /// Returns join, read-only, validation, or storage errors.
3249    #[cfg(feature = "async")]
3250    pub async fn write_global_record(&self, record: ParsedGlobalData) -> Result<()> {
3251        let world = self.blocking_clone();
3252        tokio::task::spawn_blocking(move || world.write_global_record_blocking(&record))
3253            .await
3254            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3255    }
3256
3257    /// Async wrapper for [`Self::delete_global_record_blocking`].
3258    ///
3259    /// # Errors
3260    ///
3261    /// Returns join, read-only, or storage errors.
3262    #[cfg(feature = "async")]
3263    pub async fn delete_global_record(&self, kind: GlobalRecordKind) -> Result<()> {
3264        let world = self.blocking_clone();
3265        tokio::task::spawn_blocking(move || world.delete_global_record_blocking(kind))
3266            .await
3267            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3268    }
3269
3270    /// Async wrapper for [`Self::get_heightmap_blocking`].
3271    ///
3272    /// # Errors
3273    ///
3274    /// Returns join, storage, or heightmap parse errors.
3275    #[cfg(feature = "async")]
3276    pub async fn get_heightmap(&self, pos: ChunkPos) -> Result<Option<HeightMap2d>> {
3277        let world = self.blocking_clone();
3278        tokio::task::spawn_blocking(move || world.get_heightmap_blocking(pos))
3279            .await
3280            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3281    }
3282
3283    /// Async wrapper for [`Self::put_heightmap_blocking`].
3284    ///
3285    /// # Errors
3286    ///
3287    /// Returns join, read-only, validation, or storage errors.
3288    #[cfg(feature = "async")]
3289    pub async fn put_heightmap(
3290        &self,
3291        pos: ChunkPos,
3292        version: ChunkVersion,
3293        height_map: HeightMap2d,
3294    ) -> Result<()> {
3295        let world = self.blocking_clone();
3296        tokio::task::spawn_blocking(move || world.put_heightmap_blocking(pos, version, height_map))
3297            .await
3298            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3299    }
3300
3301    /// Async wrapper for [`Self::put_biome_storage_blocking`].
3302    ///
3303    /// # Errors
3304    ///
3305    /// Returns join, read-only, validation, or storage errors.
3306    #[cfg(feature = "async")]
3307    pub async fn put_biome_storage(&self, pos: ChunkPos, biome: Biome3d) -> Result<()> {
3308        let world = self.blocking_clone();
3309        tokio::task::spawn_blocking(move || world.put_biome_storage_blocking(pos, biome))
3310            .await
3311            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3312    }
3313
3314    /// Async wrapper for [`Self::scan_hsa_records_blocking`].
3315    ///
3316    /// # Errors
3317    ///
3318    /// Returns join, storage, cancellation, or HSA parse errors.
3319    #[cfg(feature = "async")]
3320    pub async fn scan_hsa_records(
3321        &self,
3322        options: WorldScanOptions,
3323    ) -> Result<Vec<(ChunkPos, Vec<ParsedHardcodedSpawnArea>)>> {
3324        let world = self.blocking_clone();
3325        tokio::task::spawn_blocking(move || world.scan_hsa_records_blocking(options))
3326            .await
3327            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3328    }
3329
3330    /// Async wrapper for [`Self::put_hsa_for_chunk_blocking`].
3331    ///
3332    /// # Errors
3333    ///
3334    /// Returns join, read-only, validation, or storage errors.
3335    #[cfg(feature = "async")]
3336    pub async fn put_hsa_for_chunk(
3337        &self,
3338        pos: ChunkPos,
3339        areas: Vec<ParsedHardcodedSpawnArea>,
3340    ) -> Result<()> {
3341        let world = self.blocking_clone();
3342        tokio::task::spawn_blocking(move || world.put_hsa_for_chunk_blocking(pos, &areas))
3343            .await
3344            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3345    }
3346
3347    /// Async wrapper for [`Self::delete_hsa_for_chunk_blocking`].
3348    ///
3349    /// # Errors
3350    ///
3351    /// Returns join, read-only, or storage errors.
3352    #[cfg(feature = "async")]
3353    pub async fn delete_hsa_for_chunk(&self, pos: ChunkPos) -> Result<()> {
3354        let world = self.blocking_clone();
3355        tokio::task::spawn_blocking(move || world.delete_hsa_for_chunk_blocking(pos))
3356            .await
3357            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3358    }
3359
3360    /// Async wrapper for [`Self::block_entities_in_chunk_blocking`].
3361    ///
3362    /// # Errors
3363    ///
3364    /// Returns join, storage, or block-entity parse errors.
3365    #[cfg(feature = "async")]
3366    pub async fn block_entities_in_chunk(&self, pos: ChunkPos) -> Result<Vec<BlockEntityRecord>> {
3367        let world = self.blocking_clone();
3368        tokio::task::spawn_blocking(move || world.block_entities_in_chunk_blocking(pos))
3369            .await
3370            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3371    }
3372
3373    /// Async wrapper for [`Self::put_block_entities_blocking`].
3374    ///
3375    /// # Errors
3376    ///
3377    /// Returns join, read-only, validation, or storage errors.
3378    #[cfg(feature = "async")]
3379    pub async fn put_block_entities(
3380        &self,
3381        pos: ChunkPos,
3382        entities: Vec<ParsedBlockEntity>,
3383    ) -> Result<()> {
3384        let world = self.blocking_clone();
3385        tokio::task::spawn_blocking(move || world.put_block_entities_blocking(pos, &entities))
3386            .await
3387            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3388    }
3389
3390    /// Async wrapper for [`Self::edit_block_entity_at_blocking`].
3391    ///
3392    /// # Errors
3393    ///
3394    /// Returns join, read-only, validation, or storage errors.
3395    #[cfg(feature = "async")]
3396    pub async fn edit_block_entity_at<F>(
3397        &self,
3398        pos: ChunkPos,
3399        block: BlockPos,
3400        edit: F,
3401    ) -> Result<()>
3402    where
3403        F: FnOnce(&mut NbtTag) -> Result<()> + Send + 'static,
3404    {
3405        let world = self.blocking_clone();
3406        tokio::task::spawn_blocking(move || world.edit_block_entity_at_blocking(pos, block, edit))
3407            .await
3408            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3409    }
3410
3411    /// Async wrapper for [`Self::delete_block_entity_at_blocking`].
3412    ///
3413    /// # Errors
3414    ///
3415    /// Returns join, read-only, or storage errors.
3416    #[cfg(feature = "async")]
3417    pub async fn delete_block_entity_at(&self, pos: ChunkPos, block: BlockPos) -> Result<()> {
3418        let world = self.blocking_clone();
3419        tokio::task::spawn_blocking(move || world.delete_block_entity_at_blocking(pos, block))
3420            .await
3421            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3422    }
3423
3424    /// Async wrapper for [`Self::actors_in_chunk_blocking`].
3425    ///
3426    /// # Errors
3427    ///
3428    /// Returns join, storage, or actor digest validation errors.
3429    #[cfg(feature = "async")]
3430    pub async fn actors_in_chunk(&self, pos: ChunkPos) -> Result<Vec<ActorRecord>> {
3431        let world = self.blocking_clone();
3432        tokio::task::spawn_blocking(move || world.actors_in_chunk_blocking(pos))
3433            .await
3434            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3435    }
3436
3437    /// Async wrapper for [`Self::put_actor_blocking`].
3438    ///
3439    /// # Errors
3440    ///
3441    /// Returns join, read-only, validation, or storage errors.
3442    #[cfg(feature = "async")]
3443    pub async fn put_actor(&self, pos: ChunkPos, actor: ParsedEntity) -> Result<()> {
3444        let world = self.blocking_clone();
3445        tokio::task::spawn_blocking(move || world.put_actor_blocking(pos, &actor))
3446            .await
3447            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3448    }
3449
3450    /// Async wrapper for [`Self::delete_actor_blocking`].
3451    ///
3452    /// # Errors
3453    ///
3454    /// Returns join, read-only, or storage errors.
3455    #[cfg(feature = "async")]
3456    pub async fn delete_actor(&self, pos: ChunkPos, uid: ActorUid) -> Result<()> {
3457        let world = self.blocking_clone();
3458        tokio::task::spawn_blocking(move || world.delete_actor_blocking(pos, uid))
3459            .await
3460            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3461    }
3462
3463    /// Async wrapper for [`Self::move_actor_blocking`].
3464    ///
3465    /// # Errors
3466    ///
3467    /// Returns join, read-only, validation, or storage errors.
3468    #[cfg(feature = "async")]
3469    pub async fn move_actor(
3470        &self,
3471        from: ChunkPos,
3472        to: ChunkPos,
3473        actor: ParsedEntity,
3474    ) -> Result<()> {
3475        let world = self.blocking_clone();
3476        tokio::task::spawn_blocking(move || world.move_actor_blocking(from, to, &actor))
3477            .await
3478            .map_err(|error| BedrockWorldError::Join(error.to_string()))?
3479    }
3480
3481    #[cfg(feature = "async")]
3482    #[must_use]
3483    fn blocking_clone(&self) -> Self {
3484        Self {
3485            path: self.path.clone(),
3486            options: self.options.clone(),
3487            storage: self.storage.clone(),
3488            format: self.format,
3489        }
3490    }
3491
3492    /// Put raw record blocking.
3493    pub fn put_raw_record_blocking(&self, key: &ChunkKey, value: &[u8]) -> Result<()> {
3494        self.ensure_writable()?;
3495        self.storage().put(&key.encode(), value)
3496    }
3497
3498    /// Delete raw record blocking.
3499    pub fn delete_raw_record_blocking(&self, key: &ChunkKey) -> Result<()> {
3500        self.ensure_writable()?;
3501        self.storage().delete(&key.encode())
3502    }
3503
3504    #[must_use]
3505    /// Starts a buffered world transaction.
3506    pub fn transaction(&self) -> WorldTransaction<'_, S> {
3507        WorldTransaction {
3508            storage: &self.storage,
3509            batch: StorageBatch::new(),
3510            read_only: self.options.read_only,
3511        }
3512    }
3513
3514    fn ensure_writable(&self) -> Result<()> {
3515        if self.options.read_only {
3516            return Err(BedrockWorldError::ReadOnly);
3517        }
3518        Ok(())
3519    }
3520}
3521
3522/// Batched raw record and player writes for a [`BedrockWorld`].
3523pub struct WorldTransaction<'a, S = Arc<dyn WorldStorage>>
3524where
3525    S: WorldStorageHandle,
3526{
3527    storage: &'a S,
3528    batch: StorageBatch,
3529    read_only: bool,
3530}
3531
3532impl<S> WorldTransaction<'_, S>
3533where
3534    S: WorldStorageHandle,
3535{
3536    /// Stages a raw chunk record write.
3537    pub fn put_raw_record(&mut self, key: &ChunkKey, value: impl Into<Bytes>) {
3538        self.batch.put(key.encode(), value.into());
3539    }
3540
3541    /// Stages a raw chunk record delete.
3542    pub fn delete_raw_record(&mut self, key: &ChunkKey) {
3543        self.batch.delete(key.encode());
3544    }
3545
3546    /// Stages a raw key/value write.
3547    pub fn put_raw_key(&mut self, key: impl Into<Bytes>, value: impl Into<Bytes>) {
3548        self.batch.put(key.into(), value.into());
3549    }
3550
3551    /// Stages a raw key delete.
3552    pub fn delete_raw_key(&mut self, key: impl Into<Bytes>) {
3553        self.batch.delete(key.into());
3554    }
3555
3556    /// Stages a player record write using the player's storage key.
3557    ///
3558    /// # Errors
3559    ///
3560    /// Returns validation errors when the player id does not map to a `LevelDB`
3561    /// key.
3562    pub fn put_player(&mut self, player: &PlayerData) -> Result<()> {
3563        let Some(key) = player.id.storage_key() else {
3564            return Err(BedrockWorldError::Validation(
3565                "player id has no LevelDB key".to_string(),
3566            ));
3567        };
3568        self.batch
3569            .put(Bytes::copy_from_slice(key.as_ref()), player.raw.clone());
3570        Ok(())
3571    }
3572
3573    /// Stages a typed map record write after roundtrip validation.
3574    ///
3575    /// # Errors
3576    ///
3577    /// Returns validation or serialization errors for malformed map data.
3578    pub fn put_map_record(&mut self, record: &ParsedMapData) -> Result<()> {
3579        let value = encode_map_record(record)?;
3580        parse_map_record(record.record_id.clone(), value.clone())?;
3581        self.batch.put(record.record_id.storage_key(), value);
3582        Ok(())
3583    }
3584
3585    /// Stages a typed map record delete.
3586    pub fn delete_map_record(&mut self, id: &MapRecordId) {
3587        self.batch.delete(id.storage_key());
3588    }
3589
3590    /// Stages a typed global record write after roundtrip validation.
3591    ///
3592    /// # Errors
3593    ///
3594    /// Returns validation or serialization errors for malformed global data.
3595    pub fn put_global_record(&mut self, record: &ParsedGlobalData) -> Result<()> {
3596        let value = encode_global_record(record)?;
3597        parse_global_record(record.kind.clone(), record.name.clone(), value.clone())?;
3598        self.batch.put(record.kind.storage_key(), value);
3599        Ok(())
3600    }
3601
3602    /// Stages a typed global record delete.
3603    pub fn delete_global_record(&mut self, kind: &GlobalRecordKind) {
3604        self.batch.delete(kind.storage_key());
3605    }
3606
3607    /// Stages a modern actor write and updates the chunk `digp` digest.
3608    ///
3609    /// # Errors
3610    ///
3611    /// Returns validation errors for malformed actor NBT or digest data.
3612    pub fn put_actor(&mut self, pos: ChunkPos, uid: ActorUid, value: Bytes) -> Result<()> {
3613        parse_entities_from_value(&value, &mut WorldParseReport::default());
3614        self.batch.put(uid.storage_key(), value);
3615        self.replace_actor_digest(pos, |ids| {
3616            if !ids.contains(&uid) {
3617                ids.push(uid);
3618            }
3619        })?;
3620        Ok(())
3621    }
3622
3623    /// Stages a modern actor delete and removes it from the chunk `digp` digest.
3624    ///
3625    /// # Errors
3626    ///
3627    /// Returns validation errors for malformed existing digest data.
3628    pub fn delete_actor(&mut self, pos: ChunkPos, uid: ActorUid) -> Result<()> {
3629        self.batch.delete(uid.storage_key());
3630        self.replace_actor_digest(pos, |ids| ids.retain(|id| *id != uid))
3631    }
3632
3633    /// Validates and commits all staged writes atomically through the storage backend.
3634    ///
3635    /// # Errors
3636    ///
3637    /// Returns [`BedrockWorldError::ReadOnly`] for read-only worlds, validation
3638    /// errors for unsafe key/value combinations, or storage errors.
3639    pub fn commit(self) -> Result<()> {
3640        if self.read_only {
3641            return Err(BedrockWorldError::ReadOnly);
3642        }
3643        validate_batch(&self.batch)?;
3644        self.storage.storage().write_batch(&self.batch)
3645    }
3646
3647    fn replace_actor_digest<F>(&mut self, pos: ChunkPos, update: F) -> Result<()>
3648    where
3649        F: FnOnce(&mut Vec<ActorUid>),
3650    {
3651        let key = ActorDigestKey::new(pos).storage_key();
3652        let mut ids = self
3653            .storage
3654            .storage()
3655            .get(&key)?
3656            .map_or_else(|| Ok(Vec::new()), |value| parse_actor_digest_ids(&value))?;
3657        update(&mut ids);
3658        if ids.is_empty() {
3659            self.batch.delete(key);
3660        } else {
3661            self.batch.put(key, encode_actor_digest_ids(&ids));
3662        }
3663        Ok(())
3664    }
3665}
3666
3667fn validate_batch(batch: &StorageBatch) -> Result<()> {
3668    for op in batch.ops() {
3669        match op {
3670            StorageOp::Put { key, value } => {
3671                if key.is_empty() {
3672                    return Err(BedrockWorldError::Validation(
3673                        "batch contains empty key".to_string(),
3674                    ));
3675                }
3676                if value.is_empty() {
3677                    return Err(BedrockWorldError::Validation(format!(
3678                        "batch put for key {key:?} contains empty value"
3679                    )));
3680                }
3681            }
3682            StorageOp::Delete { key } => {
3683                if key.is_empty() {
3684                    return Err(BedrockWorldError::Validation(
3685                        "batch contains empty delete key".to_string(),
3686                    ));
3687                }
3688            }
3689        }
3690    }
3691    Ok(())
3692}
3693
3694fn validate_block_entities_in_chunk(pos: ChunkPos, entities: &[ParsedBlockEntity]) -> Result<()> {
3695    for entity in entities {
3696        let Some([x, y, z]) = entity.position else {
3697            return Err(BedrockWorldError::Validation(
3698                "block entity is missing x/y/z position".to_string(),
3699            ));
3700        };
3701        let block_pos = BlockPos { x, y, z };
3702        if block_pos.to_chunk_pos(pos.dimension) != pos {
3703            return Err(BedrockWorldError::Validation(format!(
3704                "block entity at {x},{y},{z} is outside chunk {pos:?}"
3705            )));
3706        }
3707    }
3708    Ok(())
3709}
3710
3711fn check_cancelled(options: &WorldScanOptions) -> Result<()> {
3712    if options
3713        .cancel
3714        .as_ref()
3715        .is_some_and(CancelFlag::is_cancelled)
3716    {
3717        return Err(BedrockWorldError::Cancelled {
3718            operation: "world scan",
3719        });
3720    }
3721    Ok(())
3722}
3723
3724fn emit_progress(options: &WorldScanOptions, entries_seen: usize) {
3725    if let Some(progress) = &options.progress {
3726        progress.emit(WorldScanProgress { entries_seen });
3727    }
3728}
3729
3730fn check_render_load_cancelled(options: &ChunkLoadOptions) -> Result<()> {
3731    if options
3732        .cancel
3733        .as_ref()
3734        .is_some_and(CancelFlag::is_cancelled)
3735    {
3736        return Err(BedrockWorldError::Cancelled {
3737            operation: "render chunk load",
3738        });
3739    }
3740    Ok(())
3741}
3742
3743fn emit_render_load_progress(options: &ChunkLoadOptions, completed_chunks: usize) {
3744    if completed_chunks.is_multiple_of(options.pipeline.resolve_progress_interval()) {
3745        if let Some(progress) = &options.progress {
3746            progress.emit(WorldScanProgress {
3747                entries_seen: completed_chunks,
3748            });
3749        }
3750    }
3751}
3752
3753fn sort_render_chunk_positions(positions: &mut [ChunkPos], priority: ChunkLoadPriority) {
3754    match priority {
3755        ChunkLoadPriority::RowMajor => positions.sort(),
3756        ChunkLoadPriority::DistanceFrom { chunk_x, chunk_z } => positions.sort_by_key(|pos| {
3757            let dx = i64::from(pos.x) - i64::from(chunk_x);
3758            let dz = i64::from(pos.z) - i64::from(chunk_z);
3759            (
3760                dx.saturating_mul(dx).saturating_add(dz.saturating_mul(dz)),
3761                pos.z,
3762                pos.x,
3763                pos.dimension,
3764            )
3765        }),
3766    }
3767}
3768
3769fn push_render_record_request(
3770    keys: &mut Vec<Bytes>,
3771    requests: &mut Vec<RenderRecordRequest>,
3772    chunk_index: usize,
3773    pos: ChunkPos,
3774    kind: RenderRecordKind,
3775) {
3776    let key = match kind {
3777        RenderRecordKind::LegacyTerrain => {
3778            ChunkKey::new(pos, ChunkRecordTag::LegacyTerrain).encode()
3779        }
3780        RenderRecordKind::Data3D => ChunkKey::new(pos, ChunkRecordTag::Data3D).encode(),
3781        RenderRecordKind::Data2D => ChunkKey::new(pos, ChunkRecordTag::Data2D).encode(),
3782        RenderRecordKind::Data2DLegacy => ChunkKey::new(pos, ChunkRecordTag::Data2DLegacy).encode(),
3783        RenderRecordKind::Subchunk(y) => ChunkKey::subchunk(pos, y).encode(),
3784        RenderRecordKind::BlockEntity => ChunkKey::new(pos, ChunkRecordTag::BlockEntity).encode(),
3785    };
3786    keys.push(key);
3787    requests.push(RenderRecordRequest { chunk_index, kind });
3788}
3789
3790fn apply_render_record_values(
3791    chunks: &mut [RawChunkData],
3792    requests: &[RenderRecordRequest],
3793    values: Vec<Option<Bytes>>,
3794) -> usize {
3795    let mut found = 0usize;
3796    for (request, value) in requests.iter().copied().zip(values) {
3797        let Some(value) = value else {
3798            continue;
3799        };
3800        found = found.saturating_add(1);
3801        let Some(chunk) = chunks.get_mut(request.chunk_index) else {
3802            continue;
3803        };
3804        match request.kind {
3805            RenderRecordKind::LegacyTerrain => {
3806                chunk.legacy_terrain = Some(value);
3807            }
3808            RenderRecordKind::Data3D => {
3809                if chunk.biome_record.is_none() {
3810                    chunk.biome_record = Some((crate::ChunkVersion::New, value));
3811                }
3812            }
3813            RenderRecordKind::Data2D | RenderRecordKind::Data2DLegacy => {
3814                if chunk.biome_record.is_none() {
3815                    chunk.biome_record = Some((crate::ChunkVersion::Old, value));
3816                }
3817            }
3818            RenderRecordKind::Subchunk(y) => {
3819                chunk.subchunks.insert(y, value);
3820            }
3821            RenderRecordKind::BlockEntity => {
3822                chunk.block_entities = Some(value);
3823            }
3824        }
3825    }
3826    found
3827}
3828
3829fn planned_render_subchunk_ys(
3830    pos: ChunkPos,
3831    options: &ChunkLoadOptions,
3832    height_map: Option<&[[Option<i16>; 16]; 16]>,
3833) -> Result<BTreeSet<i8>> {
3834    let mut subchunk_ys = BTreeSet::new();
3835    let request = options.data_request.clone();
3836    let (min_y, max_y) = pos.subchunk_index_range(crate::ChunkVersion::New);
3837    for requirement in request.subchunks {
3838        match requirement {
3839            SubchunkDataRequirement::SurfaceColumns(subchunks) => match subchunks {
3840                ExactSurfaceSubchunkPolicy::Full => {
3841                    for y in min_y..=max_y {
3842                        subchunk_ys.insert(y);
3843                    }
3844                }
3845                ExactSurfaceSubchunkPolicy::HintThenVerify => {
3846                    if let Some(height_map) = height_map {
3847                        insert_needed_surface_subchunks(
3848                            &mut subchunk_ys,
3849                            Some(height_map),
3850                            min_y,
3851                            max_y,
3852                        );
3853                    } else {
3854                        for y in min_y..=max_y {
3855                            subchunk_ys.insert(y);
3856                        }
3857                    }
3858                }
3859            },
3860            SubchunkDataRequirement::Layer(y) | SubchunkDataRequirement::CaveSlice(y) => {
3861                subchunk_ys.insert(block_y_to_subchunk_y(y)?);
3862            }
3863            SubchunkDataRequirement::Full3dIndices => {
3864                for y in min_y..=max_y {
3865                    subchunk_ys.insert(y);
3866                }
3867            }
3868        }
3869    }
3870    Ok(subchunk_ys)
3871}
3872
3873fn request_needs_biome_record(options: &ChunkLoadOptions) -> bool {
3874    let request = &options.data_request;
3875    request.height_map || !matches!(request.biome, BiomeDataRequirement::None)
3876}
3877
3878fn request_needs_legacy_terrain(options: &ChunkLoadOptions) -> bool {
3879    options.data_request.height_map
3880        || request_builds_column_samples(options)
3881        || !matches!(options.data_request.biome, BiomeDataRequirement::None)
3882}
3883
3884fn request_needs_legacy_terrain_fallback(options: &ChunkLoadOptions) -> bool {
3885    !request_needs_legacy_terrain(options) && !options.data_request.subchunks.is_empty()
3886}
3887
3888fn request_loads_block_entities(options: &ChunkLoadOptions) -> bool {
3889    options.data_request.block_entities
3890}
3891
3892fn request_builds_column_samples(options: &ChunkLoadOptions) -> bool {
3893    options
3894        .data_request
3895        .subchunks
3896        .iter()
3897        .any(|requirement| matches!(requirement, SubchunkDataRequirement::SurfaceColumns(_)))
3898}
3899
3900fn request_uses_hint_surface_subchunks(options: &ChunkLoadOptions) -> bool {
3901    options.data_request.subchunks.iter().any(|requirement| {
3902        matches!(
3903            requirement,
3904            SubchunkDataRequirement::SurfaceColumns(ExactSurfaceSubchunkPolicy::HintThenVerify)
3905        )
3906    })
3907}
3908
3909fn exact_surface_full_request(options: &mut ChunkLoadOptions) {
3910    let mut request = options.data_request.clone();
3911    for requirement in &mut request.subchunks {
3912        if matches!(
3913            requirement,
3914            SubchunkDataRequirement::SurfaceColumns(ExactSurfaceSubchunkPolicy::HintThenVerify)
3915        ) {
3916            *requirement =
3917                SubchunkDataRequirement::SurfaceColumns(ExactSurfaceSubchunkPolicy::Full);
3918        }
3919    }
3920    options.data_request = request;
3921}
3922
3923fn insert_render_biome_storages(
3924    render_biomes: &mut BTreeMap<i32, ParsedBiomeStorage>,
3925    biome_data: Option<ParsedBiomeData>,
3926    request: &ChunkDataRequest,
3927) {
3928    let Some(biome_data) = biome_data else {
3929        return;
3930    };
3931    match request.biome {
3932        BiomeDataRequirement::SurfaceColumns | BiomeDataRequirement::All => {
3933            for storage in biome_data.storages {
3934                let key = storage.y.unwrap_or(i32::MIN);
3935                render_biomes.insert(key, storage);
3936            }
3937        }
3938        BiomeDataRequirement::Layer(y) => {
3939            let mut fallback = None;
3940            for storage in biome_data.storages {
3941                if biome_storage_contains_y(&storage, y) {
3942                    render_biomes.insert(biome_storage_bucket_y(y), storage);
3943                    return;
3944                }
3945                fallback.get_or_insert(storage);
3946            }
3947            if let Some(storage) = fallback {
3948                render_biomes.insert(biome_storage_bucket_y(y), storage);
3949            }
3950        }
3951        BiomeDataRequirement::None => {}
3952    }
3953}
3954
3955fn parse_render_biome_record(
3956    record: Option<&(crate::ChunkVersion, Bytes)>,
3957) -> Result<Option<ParsedBiomeData>> {
3958    let Some((version, value)) = record else {
3959        return Ok(None);
3960    };
3961    let data = match version {
3962        crate::ChunkVersion::New => parse_data3d(value),
3963        crate::ChunkVersion::Old => parse_legacy_data2d(value),
3964    }
3965    .map_err(|error| BedrockWorldError::CorruptWorld(format!("biome data: {error}")))?;
3966    Ok(Some(data))
3967}
3968
3969fn render_height_map_from_biome_data(
3970    pos: ChunkPos,
3971    biome_data: &ParsedBiomeData,
3972) -> [[Option<i16>; 16]; 16] {
3973    let mut heights = [[None; 16]; 16];
3974    for local_z in 0..16_u8 {
3975        for local_x in 0..16_u8 {
3976            let index = height_map_index(local_x, local_z);
3977            heights[usize::from(local_z)][usize::from(local_x)] = biome_data
3978                .height_map
3979                .get(index)
3980                .and_then(|height| normalize_biome_height(pos, biome_data.version, *height));
3981        }
3982    }
3983    heights
3984}
3985
3986fn normalize_biome_height(
3987    pos: ChunkPos,
3988    version: crate::ChunkVersion,
3989    stored_height: i16,
3990) -> Option<i16> {
3991    let (min_y, _) = pos.y_range(version);
3992    i16::try_from(i32::from(stored_height) + min_y).ok()
3993}
3994
3995fn legacy_height_map_from_raw(
3996    raw_legacy_terrain: Option<&Bytes>,
3997) -> Result<Option<[[Option<i16>; 16]; 16]>> {
3998    let Some(raw_legacy_terrain) = raw_legacy_terrain else {
3999        return Ok(None);
4000    };
4001    let terrain = LegacyTerrain::parse(raw_legacy_terrain.clone())?;
4002    Ok(Some(render_height_map_from_legacy_terrain(&terrain)))
4003}
4004
4005fn render_height_map_from_legacy_terrain(terrain: &LegacyTerrain) -> [[Option<i16>; 16]; 16] {
4006    let mut heights = [[None; 16]; 16];
4007    for local_z in 0..16_u8 {
4008        for local_x in 0..16_u8 {
4009            heights[usize::from(local_z)][usize::from(local_x)] =
4010                terrain.height_at(local_x, local_z).map(i16::from);
4011        }
4012    }
4013    heights
4014}
4015
4016fn render_biomes_from_legacy_terrain(
4017    terrain: &LegacyTerrain,
4018) -> [[Option<LegacyBiomeSample>; 16]; 16] {
4019    let mut samples = [[None; 16]; 16];
4020    for local_z in 0..16_u8 {
4021        for local_x in 0..16_u8 {
4022            samples[usize::from(local_z)][usize::from(local_x)] =
4023                terrain.biome_sample_at(local_x, local_z);
4024        }
4025    }
4026    samples
4027}
4028
4029fn render_biome_colors_from_legacy_terrain(terrain: &LegacyTerrain) -> [[Option<u32>; 16]; 16] {
4030    let mut colors = [[None; 16]; 16];
4031    let samples = render_biomes_from_legacy_terrain(terrain);
4032    for local_z in 0..16 {
4033        for local_x in 0..16 {
4034            colors[local_z][local_x] = samples[local_z][local_x].map(LegacyBiomeSample::rgb_u32);
4035        }
4036    }
4037    colors
4038}
4039
4040struct SurfaceProjection<'a> {
4041    min_subchunk_y: i8,
4042    subchunks: Vec<Option<&'a SubChunk>>,
4043    roles: Vec<Option<Vec<Vec<TerrainSurfaceRole>>>>,
4044}
4045
4046impl<'a> SurfaceProjection<'a> {
4047    fn new(subchunks: &'a BTreeMap<i8, SubChunk>) -> Option<Self> {
4048        let (&min_subchunk_y, _) = subchunks.first_key_value()?;
4049        let (&max_subchunk_y, _) = subchunks.last_key_value()?;
4050        let span = i16::from(max_subchunk_y)
4051            .saturating_sub(i16::from(min_subchunk_y))
4052            .saturating_add(1);
4053        let len = usize::try_from(span).ok()?;
4054        let mut projected = vec![None; len];
4055        let mut roles = vec![None; len];
4056        for (&y, subchunk) in subchunks {
4057            let index = i16::from(y).saturating_sub(i16::from(min_subchunk_y));
4058            if let Ok(index) = usize::try_from(index) {
4059                if let Some(slot) = projected.get_mut(index) {
4060                    *slot = Some(subchunk);
4061                    roles[index] = match &subchunk.format {
4062                        crate::chunk::SubChunkFormat::Paletted { storages, .. } => Some(
4063                            storages
4064                                .iter()
4065                                .map(|storage| {
4066                                    storage
4067                                        .states
4068                                        .iter()
4069                                        .map(|state| terrain_surface_role(&state.name))
4070                                        .collect()
4071                                })
4072                                .collect(),
4073                        ),
4074                        _ => None,
4075                    };
4076                }
4077            }
4078        }
4079        Some(Self {
4080            min_subchunk_y,
4081            subchunks: projected,
4082            roles,
4083        })
4084    }
4085
4086    fn get(&self, y: i8) -> Option<&'a SubChunk> {
4087        let index = i16::from(y).checked_sub(i16::from(self.min_subchunk_y))?;
4088        self.subchunks
4089            .get(usize::try_from(index).ok()?)?
4090            .as_ref()
4091            .copied()
4092    }
4093
4094    fn role_for(
4095        &self,
4096        y: i8,
4097        storage_index: usize,
4098        palette_index: usize,
4099    ) -> Option<TerrainSurfaceRole> {
4100        let index =
4101            usize::try_from(i16::from(y).checked_sub(i16::from(self.min_subchunk_y))?).ok()?;
4102        self.roles
4103            .get(index)?
4104            .as_ref()?
4105            .get(storage_index)?
4106            .get(palette_index)
4107            .copied()
4108    }
4109}
4110
4111fn build_terrain_column_samples(
4112    pos: ChunkPos,
4113    version: crate::ChunkVersion,
4114    subchunks: &BTreeMap<i8, SubChunk>,
4115    legacy_terrain: Option<&LegacyTerrain>,
4116    height_map: Option<&[[Option<i16>; 16]; 16]>,
4117    legacy_biomes: Option<&[[Option<LegacyBiomeSample>; 16]; 16]>,
4118    render_biomes: &BTreeMap<i32, ParsedBiomeStorage>,
4119) -> Result<TerrainColumnSamples> {
4120    let mut columns = TerrainColumnSamples::new();
4121    let projection = SurfaceProjection::new(subchunks);
4122    let (min_y, max_y) = if legacy_terrain.is_some() && subchunks.is_empty() {
4123        (0, 127)
4124    } else {
4125        pos.y_range(version)
4126    };
4127
4128    for local_z in 0..16_u8 {
4129        for local_x in 0..16_u8 {
4130            if let Some(sample) = sample_column_top_down(
4131                local_x,
4132                local_z,
4133                min_y,
4134                max_y,
4135                subchunks,
4136                projection.as_ref(),
4137                legacy_terrain,
4138                height_map,
4139                legacy_biomes,
4140                render_biomes,
4141            )? {
4142                columns.set(local_x, local_z, sample);
4143            }
4144        }
4145    }
4146    Ok(columns)
4147}
4148
4149#[allow(clippy::too_many_arguments)]
4150#[allow(clippy::too_many_lines)]
4151fn sample_column_top_down(
4152    local_x: u8,
4153    local_z: u8,
4154    min_y: i32,
4155    max_y: i32,
4156    subchunks: &BTreeMap<i8, SubChunk>,
4157    projection: Option<&SurfaceProjection<'_>>,
4158    legacy_terrain: Option<&LegacyTerrain>,
4159    height_map: Option<&[[Option<i16>; 16]; 16]>,
4160    legacy_biomes: Option<&[[Option<LegacyBiomeSample>; 16]; 16]>,
4161    render_biomes: &BTreeMap<i32, ParsedBiomeStorage>,
4162) -> Result<Option<TerrainColumnSample>> {
4163    let mut overlay: Option<TerrainColumnOverlay> = None;
4164    let mut top_water: Option<(i16, BlockState, TerrainSampleSource)> = None;
4165    let mut water_depth = 0_u8;
4166    for y in (min_y..=max_y).rev() {
4167        let height = i16::try_from(y).unwrap_or(if y < 0 { i16::MIN } else { i16::MAX });
4168
4169        let subchunk_y = block_y_to_subchunk_y(y)?;
4170        let local_y = u8::try_from(y - i32::from(subchunk_y) * 16).map_err(|_| {
4171            BedrockWorldError::Validation(format!("block y={y} has invalid local subchunk offset"))
4172        })?;
4173        let mut saw_subchunk_layer = false;
4174        if let Some(subchunk) = projection.and_then(|projection| projection.get(subchunk_y)) {
4175            for entry in subchunk.visible_block_surface_states_at(local_x, local_y, local_z) {
4176                saw_subchunk_layer = true;
4177                let role = projection
4178                    .and_then(|projection| {
4179                        projection.role_for(subchunk_y, entry.storage_index, entry.palette_index)
4180                    })
4181                    .unwrap_or_else(|| terrain_surface_role(&entry.state.name));
4182                if let Some(sample) = scan_terrain_surface_state(
4183                    local_x,
4184                    local_z,
4185                    y,
4186                    height,
4187                    entry.state,
4188                    role,
4189                    TerrainSampleSource::Subchunk,
4190                    &mut overlay,
4191                    &mut top_water,
4192                    &mut water_depth,
4193                    legacy_biomes,
4194                    render_biomes,
4195                ) {
4196                    return Ok(Some(sample));
4197                }
4198            }
4199            if saw_subchunk_layer {
4200                continue;
4201            }
4202            if let Some(id) = subchunk.legacy_block_id_at(local_x, local_y, local_z) {
4203                let data = subchunk
4204                    .legacy_block_data_at(local_x, local_y, local_z)
4205                    .unwrap_or(0);
4206                let state = legacy_world_block_state(id, data);
4207                let role = terrain_surface_role(&state.name);
4208                if let Some(sample) = scan_terrain_surface_state(
4209                    local_x,
4210                    local_z,
4211                    y,
4212                    height,
4213                    &state,
4214                    role,
4215                    TerrainSampleSource::Subchunk,
4216                    &mut overlay,
4217                    &mut top_water,
4218                    &mut water_depth,
4219                    legacy_biomes,
4220                    render_biomes,
4221                ) {
4222                    return Ok(Some(sample));
4223                }
4224                continue;
4225            }
4226        }
4227
4228        if let Some((state, source)) =
4229            legacy_terrain_block_state_at(local_x, y, local_z, subchunks, legacy_terrain)
4230        {
4231            let role = terrain_surface_role(&state.name);
4232            if let Some(sample) = scan_terrain_surface_state(
4233                local_x,
4234                local_z,
4235                y,
4236                height,
4237                &state,
4238                role,
4239                source,
4240                &mut overlay,
4241                &mut top_water,
4242                &mut water_depth,
4243                legacy_biomes,
4244                render_biomes,
4245            ) {
4246                return Ok(Some(sample));
4247            }
4248        }
4249    }
4250
4251    if let Some((water_height, water_state, water_source)) = top_water {
4252        let biome = terrain_biome_at(
4253            local_x,
4254            local_z,
4255            i32::from(water_height),
4256            legacy_biomes,
4257            render_biomes,
4258        );
4259        let relief_y = raw_height_at(height_map, local_x, local_z).unwrap_or(water_height);
4260        return Ok(Some(TerrainColumnSample {
4261            surface_y: water_height,
4262            surface_block_state: water_state.clone(),
4263            relief_y,
4264            relief_block_state: water_state.clone(),
4265            overlay,
4266            water: Some(TerrainColumnWater {
4267                surface_y: water_height,
4268                block_state: water_state,
4269                depth: water_depth,
4270                underwater_y: None,
4271                underwater_block_state: None,
4272                source: water_source,
4273            }),
4274            biome,
4275            source: water_source,
4276        }));
4277    }
4278
4279    Ok(None)
4280}
4281
4282#[allow(clippy::too_many_arguments)]
4283fn scan_terrain_surface_state(
4284    local_x: u8,
4285    local_z: u8,
4286    y: i32,
4287    height: i16,
4288    state: &BlockState,
4289    role: TerrainSurfaceRole,
4290    source: TerrainSampleSource,
4291    overlay: &mut Option<TerrainColumnOverlay>,
4292    top_water: &mut Option<(i16, BlockState, TerrainSampleSource)>,
4293    water_depth: &mut u8,
4294    legacy_biomes: Option<&[[Option<LegacyBiomeSample>; 16]; 16]>,
4295    render_biomes: &BTreeMap<i32, ParsedBiomeStorage>,
4296) -> Option<TerrainColumnSample> {
4297    match role {
4298        TerrainSurfaceRole::Air => {
4299            if top_water.is_some() {
4300                *water_depth = (*water_depth).saturating_add(1);
4301            }
4302            None
4303        }
4304        TerrainSurfaceRole::Overlay => {
4305            if let Some((water_height, water_state, water_source)) = top_water.take() {
4306                let biome = terrain_biome_at(local_x, local_z, y, legacy_biomes, render_biomes);
4307                return Some(TerrainColumnSample {
4308                    surface_y: water_height,
4309                    surface_block_state: water_state.clone(),
4310                    relief_y: height,
4311                    relief_block_state: state.clone(),
4312                    overlay: overlay.take(),
4313                    water: Some(TerrainColumnWater {
4314                        surface_y: water_height,
4315                        block_state: water_state,
4316                        depth: (*water_depth).saturating_add(1),
4317                        underwater_y: Some(height),
4318                        underwater_block_state: Some(state.clone()),
4319                        source: water_source,
4320                    }),
4321                    biome,
4322                    source: water_source,
4323                });
4324            }
4325            if overlay.is_none() {
4326                *overlay = Some(TerrainColumnOverlay {
4327                    y: height,
4328                    block_state: state.clone(),
4329                    source,
4330                });
4331            }
4332            None
4333        }
4334        TerrainSurfaceRole::Water => {
4335            if top_water.is_none() {
4336                *top_water = Some((height, state.clone(), source));
4337            } else {
4338                *water_depth = (*water_depth).saturating_add(1);
4339            }
4340            None
4341        }
4342        TerrainSurfaceRole::Primary => {
4343            let biome = terrain_biome_at(local_x, local_z, y, legacy_biomes, render_biomes);
4344            if let Some((water_height, water_state, water_source)) = top_water.take() {
4345                return Some(TerrainColumnSample {
4346                    surface_y: water_height,
4347                    surface_block_state: water_state.clone(),
4348                    relief_y: height,
4349                    relief_block_state: state.clone(),
4350                    overlay: overlay.take(),
4351                    water: Some(TerrainColumnWater {
4352                        surface_y: water_height,
4353                        block_state: water_state,
4354                        depth: (*water_depth).saturating_add(1),
4355                        underwater_y: Some(height),
4356                        underwater_block_state: Some(state.clone()),
4357                        source: water_source,
4358                    }),
4359                    biome,
4360                    source: water_source,
4361                });
4362            }
4363            Some(TerrainColumnSample {
4364                surface_y: height,
4365                surface_block_state: state.clone(),
4366                relief_y: height,
4367                relief_block_state: state.clone(),
4368                overlay: overlay.take(),
4369                water: None,
4370                biome,
4371                source,
4372            })
4373        }
4374    }
4375}
4376
4377fn legacy_terrain_block_state_at(
4378    local_x: u8,
4379    y: i32,
4380    local_z: u8,
4381    subchunks: &BTreeMap<i8, SubChunk>,
4382    legacy_terrain: Option<&LegacyTerrain>,
4383) -> Option<(BlockState, TerrainSampleSource)> {
4384    let terrain = legacy_terrain?;
4385    if !(0..=127).contains(&y) {
4386        return None;
4387    }
4388    let legacy_y = u8::try_from(y).ok()?;
4389    let id = terrain.block_id_at(local_x, legacy_y, local_z)?;
4390    let data = terrain
4391        .block_data_at(local_x, legacy_y, local_z)
4392        .unwrap_or(0);
4393    let source = if subchunks.is_empty() {
4394        TerrainSampleSource::LegacyTerrain
4395    } else {
4396        TerrainSampleSource::LegacyFallback
4397    };
4398    Some((legacy_world_block_state(id, data), source))
4399}
4400
4401fn terrain_biome_at(
4402    local_x: u8,
4403    local_z: u8,
4404    y: i32,
4405    legacy_biomes: Option<&[[Option<LegacyBiomeSample>; 16]; 16]>,
4406    render_biomes: &BTreeMap<i32, ParsedBiomeStorage>,
4407) -> Option<TerrainColumnBiome> {
4408    legacy_biomes
4409        .and_then(|samples| samples[usize::from(local_z)][usize::from(local_x)])
4410        .map(TerrainColumnBiome::Legacy)
4411        .or_else(|| {
4412            render_biome_id_at(local_x, local_z, y, render_biomes).map(TerrainColumnBiome::Id)
4413        })
4414}
4415
4416fn render_biome_id_at(
4417    local_x: u8,
4418    local_z: u8,
4419    y: i32,
4420    render_biomes: &BTreeMap<i32, ParsedBiomeStorage>,
4421) -> Option<u32> {
4422    let direct = render_biomes
4423        .get(&biome_storage_bucket_y(y))
4424        .or_else(|| render_biomes.values().next())
4425        .and_then(|storage| {
4426            biome_id_from_storage(storage, local_x, local_z, y).filter(|id| *id != 0)
4427        });
4428    if direct.is_some() {
4429        return direct;
4430    }
4431    for storage in render_biomes.values().rev() {
4432        if storage.y.is_none() {
4433            if let Some(id) = storage
4434                .biome_id_at(local_x, 0, local_z)
4435                .filter(|id| *id != 0)
4436            {
4437                return Some(id);
4438            }
4439            continue;
4440        }
4441        for local_y in (0..16_u8).rev() {
4442            if let Some(id) = storage
4443                .biome_id_at(local_x, local_y, local_z)
4444                .filter(|id| *id != 0)
4445            {
4446                return Some(id);
4447            }
4448        }
4449    }
4450    None
4451}
4452
4453fn render_chunk_from_raw(
4454    raw: RawChunkData,
4455    options: &ChunkLoadOptions,
4456) -> Result<(ChunkData, ChunkDecodeTiming)> {
4457    let mut timing = ChunkDecodeTiming::default();
4458    let biome_started = Instant::now();
4459    let legacy_terrain = raw.legacy_terrain.map(LegacyTerrain::parse).transpose()?;
4460    let version = raw.biome_record.as_ref().map_or_else(
4461        || {
4462            if legacy_terrain.is_some() {
4463                crate::ChunkVersion::Old
4464            } else {
4465                crate::ChunkVersion::New
4466            }
4467        },
4468        |(version, _)| *version,
4469    );
4470    let biome_data = parse_render_biome_record(raw.biome_record.as_ref())?;
4471    let height_map = biome_data
4472        .as_ref()
4473        .map(|biome_data| render_height_map_from_biome_data(raw.pos, biome_data))
4474        .or_else(|| {
4475            legacy_terrain
4476                .as_ref()
4477                .map(render_height_map_from_legacy_terrain)
4478        });
4479    let legacy_biomes = legacy_terrain
4480        .as_ref()
4481        .map(render_biomes_from_legacy_terrain);
4482    let legacy_biome_colors = legacy_terrain
4483        .as_ref()
4484        .map(render_biome_colors_from_legacy_terrain);
4485    let mut render_biomes = BTreeMap::new();
4486    let data_request = &options.data_request;
4487    insert_render_biome_storages(&mut render_biomes, biome_data, data_request);
4488    timing.biome_parse_us = biome_started.elapsed().as_micros();
4489
4490    let mut subchunks = BTreeMap::new();
4491    let subchunk_started = Instant::now();
4492    let subchunk_decode = options.data_request.preferred_decode_mode();
4493    for (y, value) in raw.subchunks {
4494        check_render_load_cancelled(options)?;
4495        subchunks.insert(y, parse_subchunk_with_mode(y, value, subchunk_decode)?);
4496    }
4497    timing.subchunk_parse_us = subchunk_started.elapsed().as_micros();
4498
4499    let block_entity_started = Instant::now();
4500    let block_entities = if request_loads_block_entities(options) {
4501        if let Some(value) = raw.block_entities {
4502            let mut report = WorldParseReport::default();
4503            parse_block_entities_from_value(&value, &mut report)
4504                .into_iter()
4505                .map(|entity| render_block_entity_from_nbt(entity.nbt))
4506                .collect()
4507        } else {
4508            Vec::new()
4509        }
4510    } else {
4511        Vec::new()
4512    };
4513    timing.block_entity_parse_us = block_entity_started.elapsed().as_micros();
4514
4515    let surface_scan_started = Instant::now();
4516    let column_samples = if request_builds_column_samples(options) {
4517        Some(build_terrain_column_samples(
4518            raw.pos,
4519            version,
4520            &subchunks,
4521            legacy_terrain.as_ref(),
4522            height_map.as_ref(),
4523            legacy_biomes.as_ref(),
4524            &render_biomes,
4525        )?)
4526    } else {
4527        None
4528    };
4529    timing.surface_scan_us = surface_scan_started.elapsed().as_micros();
4530
4531    Ok((
4532        ChunkData {
4533            pos: raw.pos,
4534            is_loaded: height_map.is_some()
4535                || legacy_biome_colors.is_some()
4536                || legacy_biomes.is_some()
4537                || !render_biomes.is_empty()
4538                || !subchunks.is_empty()
4539                || !block_entities.is_empty()
4540                || legacy_terrain.is_some(),
4541            height_map,
4542            legacy_biomes,
4543            legacy_biome_colors,
4544            biome_data: render_biomes,
4545            subchunks,
4546            block_entities,
4547            legacy_terrain,
4548            column_samples,
4549            version,
4550        },
4551        timing,
4552    ))
4553}
4554
4555fn render_load_stats(
4556    chunks: &[ChunkData],
4557    worker_threads: usize,
4558    queue_wait_ms: u128,
4559    load_ms: u128,
4560) -> ChunkLoadStats {
4561    ChunkLoadStats {
4562        requested_chunks: chunks.len(),
4563        loaded_chunks: chunks.iter().filter(|chunk| chunk.is_loaded).count(),
4564        subchunks_decoded: chunks
4565            .iter()
4566            .map(|chunk| chunk.subchunks.len())
4567            .sum::<usize>(),
4568        worker_threads,
4569        queue_wait_ms,
4570        load_ms,
4571        keys_requested: 0,
4572        keys_found: 0,
4573        exact_get_batches: 0,
4574        prefix_scans: 0,
4575        decode_ms: 0,
4576        db_read_ms: 0,
4577        biome_parse_ms: 0,
4578        biome_parse_us: 0,
4579        subchunk_parse_ms: 0,
4580        subchunk_parse_us: 0,
4581        surface_scan_ms: 0,
4582        surface_scan_us: 0,
4583        block_entity_parse_ms: 0,
4584        block_entity_parse_us: 0,
4585        full_reload_ms: 0,
4586        legacy_terrain_records: chunks
4587            .iter()
4588            .filter(|chunk| chunk.legacy_terrain.is_some())
4589            .count(),
4590        legacy_biome_samples: chunks
4591            .iter()
4592            .filter(|chunk| chunk.legacy_biomes.is_some())
4593            .count(),
4594        legacy_biome_colors: chunks
4595            .iter()
4596            .filter(|chunk| chunk.legacy_biome_colors.is_some())
4597            .count(),
4598        terrain_source_legacy: chunks
4599            .iter()
4600            .filter(|chunk| chunk.legacy_terrain.is_some() && chunk.subchunks.is_empty())
4601            .count(),
4602        terrain_source_subchunk: chunks
4603            .iter()
4604            .filter(|chunk| !chunk.subchunks.is_empty())
4605            .count(),
4606        legacy_pocket_chunks: 0,
4607        detected_format: WorldFormat::LevelDb,
4608        computed_surface_columns: chunks
4609            .iter()
4610            .filter_map(|chunk| chunk.column_samples.as_ref())
4611            .map(TerrainColumnSamples::sampled_columns)
4612            .sum(),
4613        raw_height_mismatch_columns: chunks.iter().map(raw_height_mismatch_columns).sum(),
4614        missing_subchunk_columns: chunks.iter().map(missing_surface_columns).sum(),
4615        legacy_fallback_columns: chunks
4616            .iter()
4617            .filter_map(|chunk| chunk.column_samples.as_ref())
4618            .flat_map(TerrainColumnSamples::iter)
4619            .filter(|sample| sample.source == TerrainSampleSource::LegacyFallback)
4620            .count(),
4621        legacy_biome_preferred_columns: chunks
4622            .iter()
4623            .filter_map(|chunk| chunk.column_samples.as_ref())
4624            .flat_map(TerrainColumnSamples::iter)
4625            .filter(|sample| matches!(sample.biome, Some(TerrainColumnBiome::Legacy(_))))
4626            .count(),
4627        modern_biome_fallback_columns: chunks
4628            .iter()
4629            .filter(|chunk| chunk.legacy_biomes.is_some())
4630            .filter_map(|chunk| chunk.column_samples.as_ref())
4631            .flat_map(TerrainColumnSamples::iter)
4632            .filter(|sample| matches!(sample.biome, Some(TerrainColumnBiome::Id(_))))
4633            .count(),
4634    }
4635}
4636
4637fn log_render_load_complete(stats: &ChunkLoadStats) {
4638    log::debug!(
4639        "render chunk load complete (requested_chunks={}, loaded_chunks={}, missing_chunks={}, subchunks_decoded={}, legacy_terrain_records={}, legacy_biome_samples={}, legacy_biome_colors={}, terrain_source_legacy={}, terrain_source_subchunk={}, legacy_pocket_chunks={}, detected_format={:?}, computed_surface_columns={}, raw_height_mismatch_columns={}, missing_subchunk_columns={}, legacy_fallback_columns={}, legacy_biome_preferred_columns={}, modern_biome_fallback_columns={}, worker_threads={}, queue_wait_ms={}, load_ms={}, exact_get_batches={}, keys_requested={}, keys_found={}, prefix_scans={}, db_read_ms={}, decode_ms={}, biome_parse_ms={}, subchunk_parse_ms={}, surface_scan_ms={}, block_entity_parse_ms={}, full_reload_ms={})",
4640        stats.requested_chunks,
4641        stats.loaded_chunks,
4642        stats.requested_chunks.saturating_sub(stats.loaded_chunks),
4643        stats.subchunks_decoded,
4644        stats.legacy_terrain_records,
4645        stats.legacy_biome_samples,
4646        stats.legacy_biome_colors,
4647        stats.terrain_source_legacy,
4648        stats.terrain_source_subchunk,
4649        stats.legacy_pocket_chunks,
4650        stats.detected_format,
4651        stats.computed_surface_columns,
4652        stats.raw_height_mismatch_columns,
4653        stats.missing_subchunk_columns,
4654        stats.legacy_fallback_columns,
4655        stats.legacy_biome_preferred_columns,
4656        stats.modern_biome_fallback_columns,
4657        stats.worker_threads,
4658        stats.queue_wait_ms,
4659        stats.load_ms,
4660        stats.exact_get_batches,
4661        stats.keys_requested,
4662        stats.keys_found,
4663        stats.prefix_scans,
4664        stats.db_read_ms,
4665        stats.decode_ms,
4666        stats.biome_parse_ms,
4667        stats.subchunk_parse_ms,
4668        stats.surface_scan_ms,
4669        stats.block_entity_parse_ms,
4670        stats.full_reload_ms
4671    );
4672}
4673
4674fn world_pool(worker_count: usize) -> Result<rayon::ThreadPool> {
4675    ThreadPoolBuilder::new()
4676        .num_threads(worker_count.max(1).saturating_add(1))
4677        .thread_name(|index| format!("bedrock-world-worker-{index}"))
4678        .build()
4679        .map_err(|error| {
4680            BedrockWorldError::Validation(format!("failed to build world worker pool: {error}"))
4681        })
4682}
4683
4684fn to_storage_read_options(options: &WorldScanOptions) -> StorageReadOptions {
4685    StorageReadOptions {
4686        threading: match options.threading {
4687            WorldThreadingOptions::Auto => StorageThreadingOptions::Auto,
4688            WorldThreadingOptions::Fixed(threads) => StorageThreadingOptions::Fixed(threads),
4689            WorldThreadingOptions::Single => StorageThreadingOptions::Single,
4690        },
4691        scan_mode: match options.threading {
4692            WorldThreadingOptions::Single => StorageScanMode::Sequential,
4693            WorldThreadingOptions::Auto | WorldThreadingOptions::Fixed(_) => {
4694                StorageScanMode::ParallelTables
4695            }
4696        },
4697        cache_policy: StorageCachePolicy::Bypass,
4698        pipeline: crate::storage::StoragePipelineOptions {
4699            queue_depth: options.pipeline.queue_depth,
4700            table_batch_size: options.pipeline.chunk_batch_size,
4701            progress_interval: options.pipeline.progress_interval,
4702        },
4703        cancel: options
4704            .cancel
4705            .as_ref()
4706            .map(|cancel| StorageCancelFlag::from_shared(cancel.0.clone())),
4707        progress: options.progress.as_ref().map(|progress| {
4708            let progress = progress.clone();
4709            StorageProgressSink::new(move |storage_progress| {
4710                progress.emit(WorldScanProgress {
4711                    entries_seen: storage_progress.entries_seen,
4712                });
4713            })
4714        }),
4715    }
4716}
4717
4718fn to_render_storage_read_options(options: &ChunkLoadOptions) -> StorageReadOptions {
4719    StorageReadOptions {
4720        threading: match options.threading {
4721            WorldThreadingOptions::Auto => StorageThreadingOptions::Auto,
4722            WorldThreadingOptions::Fixed(threads) => StorageThreadingOptions::Fixed(threads),
4723            WorldThreadingOptions::Single => StorageThreadingOptions::Single,
4724        },
4725        scan_mode: StorageScanMode::Sequential,
4726        cache_policy: options.storage_cache_policy,
4727        pipeline: crate::storage::StoragePipelineOptions {
4728            queue_depth: options.pipeline.queue_depth,
4729            table_batch_size: options.pipeline.chunk_batch_size,
4730            progress_interval: options.pipeline.progress_interval,
4731        },
4732        cancel: options.cancel.as_ref().map(CancelFlag::to_storage_cancel),
4733        progress: None,
4734    }
4735}
4736
4737fn chunk_record_prefix(pos: ChunkPos) -> Bytes {
4738    let mut bytes = Vec::with_capacity(if pos.dimension == crate::Dimension::Overworld {
4739        8
4740    } else {
4741        12
4742    });
4743    bytes.extend_from_slice(&pos.x.to_le_bytes());
4744    bytes.extend_from_slice(&pos.z.to_le_bytes());
4745    if pos.dimension != crate::Dimension::Overworld {
4746        bytes.extend_from_slice(&pos.dimension.id().to_le_bytes());
4747    }
4748    Bytes::from(bytes)
4749}
4750
4751fn validate_render_region(region: WorldChunkQueryRegion) -> Result<()> {
4752    if region.min_chunk_x > region.max_chunk_x || region.min_chunk_z > region.max_chunk_z {
4753        return Err(BedrockWorldError::Validation(format!(
4754            "invalid render region: min=({}, {}) max=({}, {})",
4755            region.min_chunk_x, region.min_chunk_z, region.max_chunk_x, region.max_chunk_z
4756        )));
4757    }
4758    Ok(())
4759}
4760
4761fn render_block_entity_from_nbt(nbt: NbtTag) -> ChunkBlockEntity {
4762    let root = match &nbt {
4763        NbtTag::Compound(root) => Some(root),
4764        _ => None,
4765    };
4766    ChunkBlockEntity {
4767        id: root
4768            .and_then(|root| nbt_string_field(root, "id"))
4769            .map(ToString::to_string),
4770        position: root.and_then(|root| {
4771            Some([
4772                nbt_int_field(root, "x")?,
4773                nbt_int_field(root, "y")?,
4774                nbt_int_field(root, "z")?,
4775            ])
4776        }),
4777        nbt,
4778    }
4779}
4780
4781fn nbt_string_field<'a>(
4782    root: &'a indexmap::IndexMap<String, NbtTag>,
4783    key: &str,
4784) -> Option<&'a str> {
4785    match root.get(key) {
4786        Some(NbtTag::String(value)) => Some(value),
4787        _ => None,
4788    }
4789}
4790
4791fn nbt_int_field(root: &indexmap::IndexMap<String, NbtTag>, key: &str) -> Option<i32> {
4792    match root.get(key) {
4793        Some(NbtTag::Byte(value)) => Some(i32::from(*value)),
4794        Some(NbtTag::Short(value)) => Some(i32::from(*value)),
4795        Some(NbtTag::Int(value)) => Some(*value),
4796        Some(NbtTag::Long(value)) => i32::try_from(*value).ok(),
4797        _ => None,
4798    }
4799}
4800
4801fn detect_world_format(path: &Path, hint: WorldFormatHint) -> Result<WorldFormat> {
4802    match hint {
4803        WorldFormatHint::Auto => {
4804            if path.join("db").join("CURRENT").is_file() {
4805                return Ok(detect_leveldb_world_format(path));
4806            }
4807            if path.join("chunks.dat").is_file() {
4808                return Ok(WorldFormat::PocketChunksDat);
4809            }
4810            Err(BedrockWorldError::Validation(format!(
4811                "could not detect Bedrock world storage at {}; expected db/CURRENT or chunks.dat",
4812                path.display()
4813            )))
4814        }
4815        WorldFormatHint::LevelDb => {
4816            let current = path.join("db").join("CURRENT");
4817            if !current.is_file() {
4818                return Err(BedrockWorldError::Validation(format!(
4819                    "LevelDB world missing {}",
4820                    current.display()
4821                )));
4822            }
4823            Ok(detect_leveldb_world_format(path))
4824        }
4825        WorldFormatHint::PocketChunksDat => {
4826            let chunks = path.join("chunks.dat");
4827            if !chunks.is_file() {
4828                return Err(BedrockWorldError::Validation(format!(
4829                    "Pocket chunks.dat world missing {}",
4830                    chunks.display()
4831                )));
4832            }
4833            Ok(WorldFormat::PocketChunksDat)
4834        }
4835    }
4836}
4837
4838fn detect_leveldb_world_format(path: &Path) -> WorldFormat {
4839    let Ok(document) = read_level_dat_document(&path.join("level.dat")) else {
4840        return WorldFormat::LevelDb;
4841    };
4842    let NbtTag::Compound(root) = &document.root else {
4843        return WorldFormat::LevelDb;
4844    };
4845    let storage_version = nbt_int_field(root, "StorageVersion");
4846    let network_version = nbt_int_field(root, "NetworkVersion");
4847    if storage_version.is_some_and(|version| version <= 4)
4848        || network_version.is_some_and(|version| version <= 91)
4849    {
4850        WorldFormat::LevelDbLegacyTerrain
4851    } else {
4852        WorldFormat::LevelDb
4853    }
4854}
4855
4856#[cfg(test)]
4857mod tests {
4858    use super::*;
4859    use crate::{
4860        Dimension, HardcodedSpawnAreaKind, MemoryStorage, NbtTag, StorageBatch, StorageReadOptions,
4861        StorageScanOutcome, block_storage_index,
4862    };
4863    use indexmap::IndexMap;
4864    use std::sync::Arc;
4865
4866    #[derive(Clone)]
4867    struct KeyOnlyPlayerStorage;
4868
4869    impl WorldStorage for KeyOnlyPlayerStorage {
4870        fn get(&self, _key: &[u8]) -> Result<Option<Bytes>> {
4871            Ok(None)
4872        }
4873
4874        fn put(&self, _key: &[u8], _value: &[u8]) -> Result<()> {
4875            Err(BedrockWorldError::ReadOnly)
4876        }
4877
4878        fn delete(&self, _key: &[u8]) -> Result<()> {
4879            Err(BedrockWorldError::ReadOnly)
4880        }
4881
4882        fn for_each_key(
4883            &self,
4884            _options: StorageReadOptions,
4885            _visitor: &mut (dyn FnMut(&[u8]) -> Result<StorageVisitorControl> + Send),
4886        ) -> Result<StorageScanOutcome> {
4887            Ok(StorageScanOutcome::empty())
4888        }
4889
4890        fn for_each_prefix(
4891            &self,
4892            _prefix: &[u8],
4893            _options: StorageReadOptions,
4894            _visitor: &mut (dyn FnMut(&[u8], &Bytes) -> Result<StorageVisitorControl> + Send),
4895        ) -> Result<StorageScanOutcome> {
4896            Err(BedrockWorldError::Validation(
4897                "player listing requested values".to_string(),
4898            ))
4899        }
4900
4901        fn for_each_prefix_key(
4902            &self,
4903            prefix: &[u8],
4904            _options: StorageReadOptions,
4905            visitor: &mut (dyn FnMut(&[u8]) -> Result<StorageVisitorControl> + Send),
4906        ) -> Result<StorageScanOutcome> {
4907            assert_eq!(prefix, b"player_");
4908            let _ = visitor(b"player_12345")?;
4909            Ok(StorageScanOutcome::empty())
4910        }
4911
4912        fn write_batch(&self, _batch: &StorageBatch) -> Result<()> {
4913            Err(BedrockWorldError::ReadOnly)
4914        }
4915
4916        fn flush(&self) -> Result<()> {
4917            Ok(())
4918        }
4919    }
4920
4921    #[cfg(feature = "backend-bedrock-leveldb")]
4922    fn temp_world_dir(name: &str) -> PathBuf {
4923        use std::time::{SystemTime, UNIX_EPOCH};
4924
4925        std::env::temp_dir().join(format!(
4926            "bedrock-world-{name}-{}",
4927            SystemTime::now()
4928                .duration_since(UNIX_EPOCH)
4929                .expect("time")
4930                .as_nanos()
4931        ))
4932    }
4933
4934    fn exact_surface_request(
4935        subchunks: ExactSurfaceSubchunkPolicy,
4936        biome: ExactSurfaceBiomeLoad,
4937        block_entities: bool,
4938    ) -> ChunkDataRequest {
4939        ChunkDataRequest::new()
4940            .surface_columns(subchunks)
4941            .biome(match biome {
4942                ExactSurfaceBiomeLoad::None => BiomeDataRequirement::None,
4943                ExactSurfaceBiomeLoad::TopColumns => BiomeDataRequirement::SurfaceColumns,
4944                ExactSurfaceBiomeLoad::All => BiomeDataRequirement::All,
4945            })
4946            .block_entities_if(block_entities)
4947    }
4948
4949    #[test]
4950    fn player_listing_uses_key_only_prefix_scan() {
4951        let world = BedrockWorld::from_typed_storage(
4952            "memory",
4953            KeyOnlyPlayerStorage,
4954            OpenOptions::default(),
4955        );
4956
4957        assert_eq!(
4958            world.list_players_blocking().expect("list players"),
4959            vec![PlayerId::Xuid("12345".to_string())]
4960        );
4961    }
4962
4963    #[test]
4964    fn world_threading_validates_fixed_range_and_auto_is_not_capped_to_eight() {
4965        let expected_auto = std::thread::available_parallelism()
4966            .map_or(1, usize::from)
4967            .min(10_000);
4968        assert_eq!(
4969            WorldThreadingOptions::Auto
4970                .resolve_checked(10_000)
4971                .expect("auto threads"),
4972            expected_auto
4973        );
4974        assert_eq!(
4975            WorldThreadingOptions::Fixed(MAX_WORLD_THREADS)
4976                .resolve_checked(10_000)
4977                .expect("max fixed threads"),
4978            MAX_WORLD_THREADS
4979        );
4980        assert!(WorldThreadingOptions::Fixed(0).resolve_checked(10).is_err());
4981        assert!(
4982            WorldThreadingOptions::Fixed(MAX_WORLD_THREADS + 1)
4983                .resolve_checked(10)
4984                .is_err()
4985        );
4986    }
4987
4988    #[test]
4989    fn map_and_global_records_roundtrip_through_world_transactions() {
4990        let storage = Arc::new(MemoryStorage::new());
4991        let world = BedrockWorld::from_storage(
4992            "memory",
4993            storage.clone(),
4994            OpenOptions {
4995                read_only: false,
4996                ..OpenOptions::default()
4997            },
4998        );
4999        let map_id = MapRecordId::new("9").expect("map id");
5000        let map = ParsedMapData {
5001            id: map_id.to_string(),
5002            record_id: map_id.clone(),
5003            roots: vec![NbtTag::Compound(IndexMap::from([(
5004                "scale".to_string(),
5005                NbtTag::Byte(1),
5006            )]))],
5007            known_fields: crate::MapKnownFields::default(),
5008            pixels: None,
5009            raw: Bytes::new(),
5010        };
5011
5012        world.write_map_record_blocking(&map).expect("write map");
5013        let read_map = world
5014            .read_map_record_blocking(&map_id)
5015            .expect("read map")
5016            .expect("map exists");
5017        assert_eq!(read_map.known_fields.scale, Some(1));
5018
5019        let global = ParsedGlobalData {
5020            name: "scoreboard".to_string(),
5021            kind: GlobalRecordKind::Scoreboard,
5022            roots: vec![NbtTag::Compound(IndexMap::new())],
5023            raw: Bytes::new(),
5024        };
5025        world
5026            .write_global_record_blocking(&global)
5027            .expect("write global");
5028        assert!(
5029            world
5030                .read_global_record_blocking(GlobalRecordKind::Scoreboard)
5031                .expect("read global")
5032                .is_some()
5033        );
5034
5035        world
5036            .delete_map_record_blocking(&map_id)
5037            .expect("delete map");
5038        assert!(
5039            world
5040                .read_map_record_blocking(&map_id)
5041                .expect("read deleted")
5042                .is_none()
5043        );
5044    }
5045
5046    #[test]
5047    fn hsa_and_block_entities_roundtrip_with_chunk_validation() {
5048        let storage = Arc::new(MemoryStorage::new());
5049        let world = BedrockWorld::from_storage(
5050            "memory",
5051            storage,
5052            OpenOptions {
5053                read_only: false,
5054                ..OpenOptions::default()
5055            },
5056        );
5057        let pos = ChunkPos {
5058            x: 0,
5059            z: 0,
5060            dimension: Dimension::Overworld,
5061        };
5062        let area = ParsedHardcodedSpawnArea {
5063            kind: HardcodedSpawnAreaKind::NetherFortress,
5064            min: [0, 32, 0],
5065            max: [15, 80, 15],
5066        };
5067        world
5068            .put_hsa_for_chunk_blocking(pos, std::slice::from_ref(&area))
5069            .expect("write hsa");
5070        assert_eq!(
5071            world
5072                .scan_hsa_records_blocking(WorldScanOptions::default())
5073                .expect("scan hsa")[0]
5074                .1,
5075            vec![area]
5076        );
5077
5078        let block_entity = ParsedBlockEntity {
5079            id: Some("Chest".to_string()),
5080            position: Some([1, 64, 1]),
5081            is_movable: Some(true),
5082            custom_name: None,
5083            items: Vec::new(),
5084            nbt: NbtTag::Compound(IndexMap::from([
5085                ("id".to_string(), NbtTag::String("Chest".to_string())),
5086                ("x".to_string(), NbtTag::Int(1)),
5087                ("y".to_string(), NbtTag::Int(64)),
5088                ("z".to_string(), NbtTag::Int(1)),
5089            ])),
5090        };
5091        world
5092            .put_block_entities_blocking(pos, std::slice::from_ref(&block_entity))
5093            .expect("write block entity");
5094        assert_eq!(
5095            world
5096                .block_entities_in_chunk_blocking(pos)
5097                .expect("read block entities")[0]
5098                .entity
5099                .position,
5100            Some([1, 64, 1])
5101        );
5102    }
5103
5104    #[test]
5105    fn actor_write_updates_digest_and_prefix_together() {
5106        let storage = Arc::new(MemoryStorage::new());
5107        let world = BedrockWorld::from_storage(
5108            "memory",
5109            storage.clone(),
5110            OpenOptions {
5111                read_only: false,
5112                ..OpenOptions::default()
5113            },
5114        );
5115        let pos = ChunkPos {
5116            x: 2,
5117            z: 3,
5118            dimension: Dimension::Overworld,
5119        };
5120        let actor_nbt = NbtTag::Compound(IndexMap::from([
5121            (
5122                "identifier".to_string(),
5123                NbtTag::String("minecraft:pig".to_string()),
5124            ),
5125            ("UniqueID".to_string(), NbtTag::Long(77)),
5126            (
5127                "Pos".to_string(),
5128                NbtTag::List(vec![
5129                    NbtTag::Float(32.0),
5130                    NbtTag::Float(64.0),
5131                    NbtTag::Float(48.0),
5132                ]),
5133            ),
5134        ]));
5135        let actor = ParsedEntity {
5136            identifier: Some("minecraft:pig".to_string()),
5137            definitions: Vec::new(),
5138            unique_id: Some(77),
5139            position: Some([32.0, 64.0, 48.0]),
5140            rotation: None,
5141            motion: None,
5142            items: Vec::new(),
5143            nbt: actor_nbt,
5144        };
5145
5146        world.put_actor_blocking(pos, &actor).expect("put actor");
5147        let digest = storage
5148            .get(&ActorDigestKey::new(pos).storage_key())
5149            .expect("get digest")
5150            .expect("digest exists");
5151        assert_eq!(
5152            parse_actor_digest_ids(&digest).expect("parse digest"),
5153            vec![ActorUid(77)]
5154        );
5155        assert!(
5156            storage
5157                .get(&ActorUid(77).storage_key())
5158                .expect("get actor")
5159                .is_some()
5160        );
5161
5162        world
5163            .delete_actor_blocking(pos, ActorUid(77))
5164            .expect("delete actor");
5165        assert!(
5166            storage
5167                .get(&ActorDigestKey::new(pos).storage_key())
5168                .expect("get deleted digest")
5169                .is_none()
5170        );
5171        assert!(
5172            storage
5173                .get(&ActorUid(77).storage_key())
5174                .expect("get deleted actor")
5175                .is_none()
5176        );
5177    }
5178
5179    #[test]
5180    fn render_chunk_priority_distance_orders_from_center() {
5181        let mut positions = vec![
5182            ChunkPos {
5183                x: 12,
5184                z: 0,
5185                dimension: Dimension::Overworld,
5186            },
5187            ChunkPos {
5188                x: 1,
5189                z: 0,
5190                dimension: Dimension::Overworld,
5191            },
5192            ChunkPos {
5193                x: -3,
5194                z: 0,
5195                dimension: Dimension::Overworld,
5196            },
5197            ChunkPos {
5198                x: 0,
5199                z: 0,
5200                dimension: Dimension::Overworld,
5201            },
5202        ];
5203
5204        sort_render_chunk_positions(
5205            &mut positions,
5206            ChunkLoadPriority::DistanceFrom {
5207                chunk_x: 0,
5208                chunk_z: 0,
5209            },
5210        );
5211
5212        let ordered = positions
5213            .iter()
5214            .map(|pos| (pos.x, pos.z))
5215            .collect::<Vec<_>>();
5216        assert_eq!(ordered, vec![(0, 0), (1, 0), (-3, 0), (12, 0)]);
5217    }
5218
5219    #[test]
5220    fn world_pipeline_options_resolve_automatic_bounds() {
5221        let options = WorldPipelineOptions::default();
5222
5223        assert!(options.resolve_queue_depth(4, 64) >= 1);
5224        assert_eq!(options.resolve_progress_interval(), 256);
5225
5226        let explicit = WorldPipelineOptions {
5227            queue_depth: 7,
5228            progress_interval: 9,
5229            ..WorldPipelineOptions::default()
5230        };
5231        assert_eq!(explicit.resolve_queue_depth(4, 64), 7);
5232        assert_eq!(explicit.resolve_progress_interval(), 9);
5233    }
5234
5235    #[test]
5236    fn generic_memory_storage_matches_dynamic_storage_queries() {
5237        let storage = MemoryStorage::new();
5238        storage
5239            .put(b"~local_player", b"local")
5240            .expect("put local player");
5241        storage
5242            .put(b"player_remote", b"remote")
5243            .expect("put remote player");
5244
5245        let generic_world =
5246            BedrockWorld::from_typed_storage("memory", storage.clone(), OpenOptions::default());
5247        let dynamic_world = BedrockWorld::from_storage(
5248            "memory",
5249            Arc::new(storage) as Arc<dyn WorldStorage>,
5250            OpenOptions::default(),
5251        );
5252
5253        assert_eq!(
5254            generic_world.list_players_blocking().expect("generic"),
5255            dynamic_world.list_players_blocking().expect("dynamic")
5256        );
5257        assert_eq!(
5258            generic_world
5259                .classify_keys_blocking(WorldScanOptions::default())
5260                .expect("generic classify"),
5261            dynamic_world
5262                .classify_keys_blocking(WorldScanOptions::default())
5263                .expect("dynamic classify")
5264        );
5265    }
5266
5267    #[cfg(feature = "backend-bedrock-leveldb")]
5268    #[test]
5269    fn generic_leveldb_storage_matches_dynamic_storage_queries() {
5270        let temp = temp_world_dir("generic-leveldb");
5271        std::fs::create_dir_all(&temp).expect("temp dir");
5272        let db_path = temp.join("db");
5273        let db = bedrock_leveldb::Db::open(&db_path, bedrock_leveldb::OpenOptions::default())
5274            .expect("initialize db");
5275        drop(db);
5276        let storage = BedrockLevelDbStorage::open(&db_path).expect("open storage");
5277        storage
5278            .put(b"~local_player", b"local")
5279            .expect("put local player");
5280        storage
5281            .put(b"player_remote", b"remote")
5282            .expect("put remote player");
5283        storage.flush().expect("flush");
5284
5285        let generic_world =
5286            BedrockWorld::from_typed_storage(&temp, storage.clone(), OpenOptions::default());
5287        let dynamic_world = BedrockWorld::from_storage(
5288            &temp,
5289            Arc::new(storage) as Arc<dyn WorldStorage>,
5290            OpenOptions::default(),
5291        );
5292
5293        assert_eq!(
5294            generic_world.list_players_blocking().expect("generic"),
5295            dynamic_world.list_players_blocking().expect("dynamic")
5296        );
5297        assert_eq!(
5298            generic_world
5299                .classify_keys_blocking(WorldScanOptions::default())
5300                .expect("generic classify"),
5301            dynamic_world
5302                .classify_keys_blocking(WorldScanOptions::default())
5303                .expect("dynamic classify")
5304        );
5305        std::fs::remove_dir_all(temp).expect("cleanup");
5306    }
5307
5308    #[test]
5309    fn transaction_respects_read_only_option() {
5310        let pos = ChunkPos {
5311            x: 0,
5312            z: 0,
5313            dimension: Dimension::Overworld,
5314        };
5315        let key = ChunkKey::new(pos, ChunkRecordTag::Version);
5316        let encoded = key.encode();
5317        let storage = Arc::new(MemoryStorage::new());
5318        let read_only_world =
5319            BedrockWorld::from_storage("memory", storage.clone(), OpenOptions::default());
5320        let mut transaction = read_only_world.transaction();
5321        transaction.put_raw_record(&key, Bytes::from_static(b"\x01"));
5322
5323        let error = transaction.commit().expect_err("read-only commit");
5324
5325        assert_eq!(error.kind(), crate::BedrockWorldErrorKind::ReadOnly);
5326        assert_eq!(storage.get(&encoded).expect("get"), None);
5327
5328        let writable_world = BedrockWorld::from_storage(
5329            "memory",
5330            storage.clone(),
5331            OpenOptions {
5332                read_only: false,
5333                ..OpenOptions::default()
5334            },
5335        );
5336        let mut transaction = writable_world.transaction();
5337        transaction.put_raw_record(&key, Bytes::from_static(b"\x02"));
5338        transaction.commit().expect("writable commit");
5339
5340        assert_eq!(
5341            storage.get(&encoded).expect("get"),
5342            Some(Bytes::from_static(b"\x02"))
5343        );
5344    }
5345
5346    #[test]
5347    fn biome_and_height_queries_read_legacy_data2d_in_zx_column_order() {
5348        let pos = ChunkPos {
5349            x: 0,
5350            z: 0,
5351            dimension: Dimension::Overworld,
5352        };
5353        let storage = Arc::new(MemoryStorage::new());
5354        storage
5355            .put(
5356                &ChunkKey::new(pos, ChunkRecordTag::Data2D).encode(),
5357                &test_asymmetric_data2d_bytes(),
5358            )
5359            .expect("put Data2D");
5360        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
5361
5362        assert_eq!(
5363            world
5364                .get_biome_id_blocking(pos, 3, 2, 64)
5365                .expect("biome id"),
5366            Some(32)
5367        );
5368        assert_eq!(
5369            world
5370                .get_biome_id_blocking(pos, 2, 3, 64)
5371                .expect("biome id"),
5372            Some(23)
5373        );
5374        assert_eq!(
5375            world.get_height_at_blocking(pos, 3, 2).expect("height"),
5376            Some(132)
5377        );
5378        assert_eq!(
5379            world.get_height_at_blocking(pos, 2, 3).expect("height"),
5380            Some(123)
5381        );
5382    }
5383
5384    #[test]
5385    fn data3d_height_map_is_normalized_to_dimension_min_y() {
5386        let pos = ChunkPos {
5387            x: 0,
5388            z: 0,
5389            dimension: Dimension::Overworld,
5390        };
5391        let storage = Arc::new(MemoryStorage::new());
5392        storage
5393            .put(
5394                &ChunkKey::new(pos, ChunkRecordTag::Data3D).encode(),
5395                &test_data3d_height_bytes(130),
5396            )
5397            .expect("put Data3D");
5398        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
5399
5400        assert_eq!(
5401            world.get_height_at_blocking(pos, 4, 2).expect("height"),
5402            Some(66)
5403        );
5404        let chunk = world
5405            .query_chunk_data_blocking(
5406                pos,
5407                ChunkLoadOptions {
5408                    data_request: ChunkDataRequest::new().height_map(),
5409                    ..ChunkLoadOptions::default()
5410                },
5411            )
5412            .expect("load render chunk");
5413
5414        assert_eq!(
5415            chunk.height_map.expect("height map")[usize::from(2_u8)][usize::from(4_u8)],
5416            Some(66)
5417        );
5418        assert!(chunk.column_samples.is_none());
5419    }
5420
5421    #[test]
5422    fn render_chunk_exact_load_preserves_data2d_xz_height_and_biome_coordinates() {
5423        let pos = ChunkPos {
5424            x: 0,
5425            z: 0,
5426            dimension: Dimension::Overworld,
5427        };
5428        let storage = Arc::new(MemoryStorage::new());
5429        storage
5430            .put(
5431                &ChunkKey::new(pos, ChunkRecordTag::Data2D).encode(),
5432                &test_asymmetric_data2d_bytes(),
5433            )
5434            .expect("put Data2D");
5435        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
5436
5437        let chunk = world
5438            .query_chunk_data_blocking(pos, ChunkLoadOptions::default())
5439            .expect("load render chunk");
5440        let height_map = chunk.height_map.as_ref().expect("height map");
5441        let biome_storage = chunk
5442            .biome_data
5443            .values()
5444            .next()
5445            .expect("render biome storage");
5446
5447        assert_eq!(height_map[3][1], Some(113));
5448        assert_eq!(height_map[1][3], Some(131));
5449        assert_eq!(biome_storage.biome_id_at(1, 0, 3), Some(13));
5450        assert_eq!(biome_storage.biome_id_at(3, 0, 1), Some(31));
5451    }
5452
5453    #[test]
5454    fn subchunk_layer_query_uses_block_y() {
5455        let pos = ChunkPos {
5456            x: 0,
5457            z: 0,
5458            dimension: Dimension::Overworld,
5459        };
5460        let storage = Arc::new(MemoryStorage::new());
5461        storage
5462            .put(&ChunkKey::subchunk(pos, -1).encode(), &[8, 0])
5463            .expect("put subchunk");
5464        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
5465
5466        let subchunk = world
5467            .get_subchunk_layer_blocking(pos, -1, SubChunkDecodeMode::CountsOnly)
5468            .expect("query")
5469            .expect("subchunk");
5470        assert_eq!(subchunk.y, -1);
5471    }
5472
5473    #[test]
5474    fn render_chunk_needed_surface_subchunks_avoids_full_y_range() {
5475        let pos = ChunkPos {
5476            x: 0,
5477            z: 0,
5478            dimension: Dimension::Overworld,
5479        };
5480        let storage = Arc::new(MemoryStorage::new());
5481        storage
5482            .put(
5483                &ChunkKey::new(pos, ChunkRecordTag::Data2D).encode(),
5484                &test_data2d_bytes(65, 7),
5485            )
5486            .expect("put Data2D");
5487        storage
5488            .put(
5489                &ChunkKey::subchunk(pos, 4).encode(),
5490                &test_surface_subchunk_bytes(),
5491            )
5492            .expect("put subchunk");
5493        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
5494
5495        let needed = world
5496            .query_chunk_data_blocking(
5497                pos,
5498                ChunkLoadOptions {
5499                    data_request: exact_surface_request(
5500                        ExactSurfaceSubchunkPolicy::HintThenVerify,
5501                        ExactSurfaceBiomeLoad::TopColumns,
5502                        false,
5503                    ),
5504                    ..ChunkLoadOptions::default()
5505                },
5506            )
5507            .expect("needed render chunk");
5508        let full = world
5509            .query_chunk_data_blocking(
5510                pos,
5511                ChunkLoadOptions {
5512                    data_request: exact_surface_request(
5513                        ExactSurfaceSubchunkPolicy::Full,
5514                        ExactSurfaceBiomeLoad::TopColumns,
5515                        false,
5516                    ),
5517                    ..ChunkLoadOptions::default()
5518                },
5519            )
5520            .expect("full render chunk");
5521        assert!(needed.subchunks.contains_key(&4));
5522        assert_eq!(needed.subchunks.get(&4), full.subchunks.get(&4));
5523        assert!(needed.subchunks.len() <= full.subchunks.len());
5524    }
5525
5526    #[test]
5527    fn render_chunk_needed_surface_subchunks_include_lookup_above_heightmap() {
5528        let pos = ChunkPos {
5529            x: 0,
5530            z: 0,
5531            dimension: Dimension::Overworld,
5532        };
5533        let storage = Arc::new(MemoryStorage::new());
5534        storage
5535            .put(
5536                &ChunkKey::new(pos, ChunkRecordTag::Data2D).encode(),
5537                &test_data2d_bytes(64, 7),
5538            )
5539            .expect("put Data2D");
5540        storage
5541            .put(
5542                &ChunkKey::subchunk(pos, 4).encode(),
5543                &test_uniform_named_subchunk_bytes("minecraft:stone"),
5544            )
5545            .expect("put heightmap subchunk");
5546        storage
5547            .put(
5548                &ChunkKey::subchunk(pos, 5).encode(),
5549                &test_uniform_named_subchunk_bytes("minecraft:oak_leaves"),
5550            )
5551            .expect("put upper subchunk");
5552        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
5553
5554        let chunk = world
5555            .query_chunk_data_blocking(
5556                pos,
5557                ChunkLoadOptions {
5558                    data_request: exact_surface_request(
5559                        ExactSurfaceSubchunkPolicy::HintThenVerify,
5560                        ExactSurfaceBiomeLoad::TopColumns,
5561                        false,
5562                    ),
5563                    ..ChunkLoadOptions::default()
5564                },
5565            )
5566            .expect("needed render chunk");
5567
5568        assert!(chunk.subchunks.contains_key(&4));
5569        assert!(chunk.subchunks.contains_key(&5));
5570        assert!(!chunk.subchunks.contains_key(&9));
5571        let sample = chunk
5572            .column_sample_at(0, 0)
5573            .expect("computed surface sample");
5574        assert_eq!(sample.surface_y, 95);
5575        assert_eq!(sample.surface_block_state.name, "minecraft:oak_leaves");
5576    }
5577
5578    #[test]
5579    fn render_chunk_needed_exact_surface_reloads_full_when_window_top_is_touched() {
5580        let pos = ChunkPos {
5581            x: 0,
5582            z: 0,
5583            dimension: Dimension::Overworld,
5584        };
5585        let storage = Arc::new(MemoryStorage::new());
5586        storage
5587            .put(
5588                &ChunkKey::new(pos, ChunkRecordTag::Data2D).encode(),
5589                &test_data2d_bytes(64, 7),
5590            )
5591            .expect("put Data2D");
5592        storage
5593            .put(
5594                &ChunkKey::subchunk(pos, 8).encode(),
5595                &test_uniform_named_subchunk_bytes("minecraft:stone"),
5596            )
5597            .expect("put window-top subchunk");
5598        storage
5599            .put(
5600                &ChunkKey::subchunk(pos, 9).encode(),
5601                &test_uniform_named_subchunk_bytes("minecraft:oak_leaves"),
5602            )
5603            .expect("put hidden upper subchunk");
5604        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
5605
5606        let chunk = world
5607            .query_chunk_data_blocking(
5608                pos,
5609                ChunkLoadOptions {
5610                    data_request: exact_surface_request(
5611                        ExactSurfaceSubchunkPolicy::HintThenVerify,
5612                        ExactSurfaceBiomeLoad::TopColumns,
5613                        false,
5614                    ),
5615                    ..ChunkLoadOptions::default()
5616                },
5617            )
5618            .expect("needed render chunk");
5619
5620        assert!(chunk.subchunks.contains_key(&8));
5621        assert!(chunk.subchunks.contains_key(&9));
5622        let sample = chunk
5623            .column_sample_at(0, 0)
5624            .expect("computed surface sample");
5625        assert_eq!(sample.surface_y, 159);
5626        assert_eq!(sample.surface_block_state.name, "minecraft:oak_leaves");
5627    }
5628
5629    #[test]
5630    fn render_chunk_needed_exact_surface_reloads_full_when_raw_height_is_stale() {
5631        let pos = ChunkPos {
5632            x: 0,
5633            z: 0,
5634            dimension: Dimension::Overworld,
5635        };
5636        let storage = Arc::new(MemoryStorage::new());
5637        storage
5638            .put(
5639                &ChunkKey::new(pos, ChunkRecordTag::Data2D).encode(),
5640                &test_data2d_bytes(0, 7),
5641            )
5642            .expect("put stale Data2D");
5643        storage
5644            .put(
5645                &ChunkKey::subchunk(pos, 0).encode(),
5646                &test_uniform_named_subchunk_bytes("minecraft:stone"),
5647            )
5648            .expect("put stale-height subchunk");
5649        storage
5650            .put(
5651                &ChunkKey::subchunk(pos, 4).encode(),
5652                &test_uniform_named_subchunk_bytes("minecraft:air"),
5653            )
5654            .expect("put high empty hint-window subchunk");
5655        storage
5656            .put(
5657                &ChunkKey::subchunk(pos, 10).encode(),
5658                &test_uniform_named_subchunk_bytes("minecraft:oak_leaves"),
5659            )
5660            .expect("put true roof subchunk");
5661        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
5662
5663        let chunk = world
5664            .query_chunk_data_blocking(
5665                pos,
5666                ChunkLoadOptions {
5667                    data_request: exact_surface_request(
5668                        ExactSurfaceSubchunkPolicy::HintThenVerify,
5669                        ExactSurfaceBiomeLoad::TopColumns,
5670                        false,
5671                    ),
5672                    ..ChunkLoadOptions::default()
5673                },
5674            )
5675            .expect("needed render chunk");
5676
5677        assert!(chunk.subchunks.contains_key(&10));
5678        let sample = chunk
5679            .column_sample_at(0, 0)
5680            .expect("computed surface sample");
5681        assert_eq!(sample.surface_y, 175);
5682        assert_eq!(sample.surface_block_state.name, "minecraft:oak_leaves");
5683    }
5684
5685    #[test]
5686    fn render_chunk_raw_heightmap_request_does_not_build_surface_samples() {
5687        let pos = ChunkPos {
5688            x: 0,
5689            z: 0,
5690            dimension: Dimension::Overworld,
5691        };
5692        let storage = Arc::new(MemoryStorage::new());
5693        storage
5694            .put(
5695                &ChunkKey::new(pos, ChunkRecordTag::Data2D).encode(),
5696                &test_data2d_bytes(0, 7),
5697            )
5698            .expect("put raw height");
5699        storage
5700            .put(
5701                &ChunkKey::subchunk(pos, 10).encode(),
5702                &test_uniform_named_subchunk_bytes("minecraft:oak_leaves"),
5703            )
5704            .expect("put high surface subchunk");
5705        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
5706
5707        let chunk = world
5708            .query_chunk_data_blocking(
5709                pos,
5710                ChunkLoadOptions {
5711                    data_request: ChunkDataRequest::new().height_map(),
5712                    ..ChunkLoadOptions::default()
5713                },
5714            )
5715            .expect("load raw heightmap chunk");
5716
5717        assert_eq!(chunk.height_map.as_ref().unwrap()[0][0], Some(0));
5718        assert!(chunk.column_samples.is_none());
5719        assert!(chunk.subchunks.is_empty());
5720    }
5721
5722    #[test]
5723    fn render_chunk_needed_surface_subchunks_fall_back_to_full_without_heightmap() {
5724        let pos = ChunkPos {
5725            x: 0,
5726            z: 0,
5727            dimension: Dimension::Overworld,
5728        };
5729        let storage = Arc::new(MemoryStorage::new());
5730        storage
5731            .put(
5732                &ChunkKey::subchunk(pos, 5).encode(),
5733                &test_uniform_named_subchunk_bytes("minecraft:oak_leaves"),
5734            )
5735            .expect("put upper subchunk");
5736        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
5737
5738        let chunk = world
5739            .query_chunk_data_blocking(
5740                pos,
5741                ChunkLoadOptions {
5742                    data_request: exact_surface_request(
5743                        ExactSurfaceSubchunkPolicy::HintThenVerify,
5744                        ExactSurfaceBiomeLoad::TopColumns,
5745                        false,
5746                    ),
5747                    ..ChunkLoadOptions::default()
5748                },
5749            )
5750            .expect("needed render chunk");
5751
5752        assert!(chunk.subchunks.contains_key(&5));
5753        let sample = chunk
5754            .column_sample_at(0, 0)
5755            .expect("computed surface sample");
5756        assert_eq!(sample.surface_y, 95);
5757        assert_eq!(sample.surface_block_state.name, "minecraft:oak_leaves");
5758    }
5759
5760    #[test]
5761    fn render_chunk_loads_block_entities_when_requested() {
5762        let pos = ChunkPos {
5763            x: 0,
5764            z: 0,
5765            dimension: Dimension::Overworld,
5766        };
5767        let storage = Arc::new(MemoryStorage::new());
5768        let block_entity = NbtTag::Compound(IndexMap::from([
5769            ("id".to_string(), NbtTag::String("Banner".to_string())),
5770            ("x".to_string(), NbtTag::Int(3)),
5771            ("y".to_string(), NbtTag::Int(65)),
5772            ("z".to_string(), NbtTag::Int(4)),
5773        ]));
5774        storage
5775            .put(
5776                &ChunkKey::new(pos, ChunkRecordTag::BlockEntity).encode(),
5777                &crate::nbt::serialize_root_nbt(&block_entity).expect("serialize block entity"),
5778            )
5779            .expect("put block entity");
5780        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
5781
5782        let without_entities = world
5783            .query_chunk_data_blocking(pos, ChunkLoadOptions::default())
5784            .expect("load render chunk without block entities");
5785        let with_entities = world
5786            .query_chunk_data_blocking(
5787                pos,
5788                ChunkLoadOptions {
5789                    data_request: exact_surface_request(
5790                        ExactSurfaceSubchunkPolicy::Full,
5791                        ExactSurfaceBiomeLoad::TopColumns,
5792                        true,
5793                    ),
5794                    ..ChunkLoadOptions::default()
5795                },
5796            )
5797            .expect("load render chunk with block entities");
5798
5799        assert!(without_entities.block_entities.is_empty());
5800        assert_eq!(with_entities.block_entities.len(), 1);
5801        assert_eq!(
5802            with_entities.block_entities[0].id.as_deref(),
5803            Some("Banner")
5804        );
5805        assert_eq!(with_entities.block_entities[0].position, Some([3, 65, 4]));
5806    }
5807
5808    #[test]
5809    fn surface_column_query_returns_top_block_and_water_context() {
5810        let pos = ChunkPos {
5811            x: 0,
5812            z: 0,
5813            dimension: Dimension::Overworld,
5814        };
5815        let storage = Arc::new(MemoryStorage::new());
5816        storage
5817            .put(
5818                &ChunkKey::new(pos, ChunkRecordTag::Data2D).encode(),
5819                &test_data2d_bytes(65, 7),
5820            )
5821            .expect("put Data2D");
5822        storage
5823            .put(
5824                &ChunkKey::subchunk(pos, 4).encode(),
5825                &test_surface_subchunk_bytes(),
5826            )
5827            .expect("put subchunk");
5828        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
5829
5830        let column = world
5831            .get_surface_column_blocking(pos, 0, 0, SurfaceColumnOptions::default())
5832            .expect("surface query")
5833            .expect("surface column");
5834
5835        assert_eq!(column.y, 65);
5836        assert_eq!(column.block_name, "minecraft:water");
5837        assert_eq!(column.biome_id, Some(7));
5838        assert_eq!(column.water_depth, 1);
5839        assert_eq!(
5840            column.under_water_block_name.as_deref(),
5841            Some("minecraft:sand")
5842        );
5843    }
5844
5845    #[test]
5846    fn chunk_bounds_and_nearest_loaded_chunk_use_key_only_scan() {
5847        let storage = Arc::new(MemoryStorage::new());
5848        let positions = [
5849            ChunkPos {
5850                x: -4,
5851                z: 3,
5852                dimension: Dimension::Overworld,
5853            },
5854            ChunkPos {
5855                x: 2,
5856                z: -1,
5857                dimension: Dimension::Overworld,
5858            },
5859            ChunkPos {
5860                x: 9,
5861                z: 9,
5862                dimension: Dimension::Nether,
5863            },
5864        ];
5865        for pos in positions {
5866            storage
5867                .put(&ChunkKey::new(pos, ChunkRecordTag::Version).encode(), &[1])
5868                .expect("put chunk version");
5869        }
5870        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
5871
5872        let bounds = world
5873            .discover_chunk_bounds_blocking(Dimension::Overworld, WorldScanOptions::default())
5874            .expect("bounds")
5875            .expect("overworld bounds");
5876        assert_eq!(bounds.min_chunk_x, -4);
5877        assert_eq!(bounds.max_chunk_z, 3);
5878        assert_eq!(bounds.chunk_count, 2);
5879
5880        let nearest = world
5881            .nearest_loaded_chunk_to_spawn_blocking(
5882                Dimension::Overworld,
5883                0,
5884                0,
5885                WorldScanOptions::default(),
5886            )
5887            .expect("nearest")
5888            .expect("nearest chunk");
5889        assert_eq!(nearest.x, 2);
5890        assert_eq!(nearest.z, -1);
5891    }
5892
5893    #[test]
5894    #[allow(clippy::similar_names)]
5895    fn render_region_index_uses_key_only_scan_and_parallel_load_keeps_order() {
5896        let storage = Arc::new(MemoryStorage::new());
5897        let render_positions = [
5898            ChunkPos {
5899                x: 0,
5900                z: 0,
5901                dimension: Dimension::Overworld,
5902            },
5903            ChunkPos {
5904                x: 1,
5905                z: 0,
5906                dimension: Dimension::Overworld,
5907            },
5908        ];
5909        for pos in render_positions {
5910            storage
5911                .put(
5912                    &ChunkKey::new(pos, ChunkRecordTag::Data2D).encode(),
5913                    &test_data2d_bytes(64, 3),
5914                )
5915                .expect("put render chunk");
5916        }
5917        storage
5918            .put(
5919                &ChunkKey::new(
5920                    ChunkPos {
5921                        x: 2,
5922                        z: 0,
5923                        dimension: Dimension::Overworld,
5924                    },
5925                    ChunkRecordTag::Version,
5926                )
5927                .encode(),
5928                &[1],
5929            )
5930            .expect("put non-render chunk");
5931        storage
5932            .put(
5933                &ChunkKey::new(
5934                    ChunkPos {
5935                        x: 0,
5936                        z: 0,
5937                        dimension: Dimension::Nether,
5938                    },
5939                    ChunkRecordTag::Data2D,
5940                )
5941                .encode(),
5942                &test_data2d_bytes(64, 3),
5943            )
5944            .expect("put nether chunk");
5945
5946        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
5947        let visible = world
5948            .list_chunk_positions_in_region_blocking(
5949                WorldChunkQueryRegion {
5950                    dimension: Dimension::Overworld,
5951                    min_chunk_x: 0,
5952                    min_chunk_z: 0,
5953                    max_chunk_x: 2,
5954                    max_chunk_z: 0,
5955                },
5956                WorldScanOptions {
5957                    threading: WorldThreadingOptions::Fixed(2),
5958                    ..WorldScanOptions::default()
5959                },
5960            )
5961            .expect("render region index");
5962
5963        assert_eq!(visible, render_positions.to_vec());
5964
5965        let chunks = world
5966            .query_chunk_data_many_blocking(
5967                visible,
5968                ChunkLoadOptions {
5969                    threading: WorldThreadingOptions::Fixed(2),
5970                    ..ChunkLoadOptions::default()
5971                },
5972            )
5973            .expect("parallel render chunk load");
5974        assert_eq!(
5975            chunks.iter().map(|chunk| chunk.pos).collect::<Vec<_>>(),
5976            render_positions.to_vec()
5977        );
5978    }
5979
5980    #[test]
5981    fn legacy_terrain_is_renderable_and_exact_batch_loaded() {
5982        let storage = Arc::new(MemoryStorage::new());
5983        let pos = ChunkPos {
5984            x: 0,
5985            z: 0,
5986            dimension: Dimension::Overworld,
5987        };
5988        storage
5989            .put(
5990                &ChunkKey::new(pos, ChunkRecordTag::LegacyTerrain).encode(),
5991                &test_legacy_terrain_bytes(2, 65),
5992            )
5993            .expect("put legacy terrain");
5994        let world = BedrockWorld::from_storage_with_format(
5995            "memory",
5996            storage,
5997            OpenOptions::default(),
5998            WorldFormat::LevelDbLegacyTerrain,
5999        );
6000
6001        let positions = world
6002            .list_chunk_positions_in_region_blocking(
6003                WorldChunkQueryRegion {
6004                    dimension: Dimension::Overworld,
6005                    min_chunk_x: 0,
6006                    min_chunk_z: 0,
6007                    max_chunk_x: 0,
6008                    max_chunk_z: 0,
6009                },
6010                WorldScanOptions::default(),
6011            )
6012            .expect("legacy render index");
6013        assert_eq!(positions, vec![pos]);
6014
6015        let (chunks, stats) = world
6016            .query_chunk_data_with_stats_blocking(
6017                positions,
6018                ChunkLoadOptions {
6019                    threading: WorldThreadingOptions::Single,
6020                    ..ChunkLoadOptions::default()
6021                },
6022            )
6023            .expect("legacy exact render load");
6024        assert_eq!(chunks.len(), 1);
6025        assert!(chunks[0].is_loaded);
6026        assert!(chunks[0].legacy_terrain.is_some());
6027        assert_eq!(chunks[0].height_map.as_ref().unwrap()[0][0], Some(65));
6028        assert!(chunks[0].legacy_biomes.is_some());
6029        assert!(chunks[0].legacy_biome_colors.is_some());
6030        assert_eq!(stats.prefix_scans, 0);
6031        assert_eq!(stats.legacy_terrain_records, 1);
6032        assert_eq!(stats.legacy_biome_samples, 1);
6033        assert_eq!(stats.legacy_biome_colors, 1);
6034        assert_eq!(stats.terrain_source_legacy, 1);
6035        assert_eq!(stats.detected_format, WorldFormat::LevelDbLegacyTerrain);
6036    }
6037
6038    #[test]
6039    fn legacy_terrain_biome_rgb_takes_priority_over_data2d_biome_id() {
6040        let storage = Arc::new(MemoryStorage::new());
6041        let pos = ChunkPos {
6042            x: 0,
6043            z: 0,
6044            dimension: Dimension::Overworld,
6045        };
6046        let mut terrain = test_legacy_terrain_bytes(2, 65);
6047        write_legacy_biome_sample(&mut terrain, 0, 0, 12, 0x0034_a853);
6048        storage
6049            .put(
6050                &ChunkKey::new(pos, ChunkRecordTag::LegacyTerrain).encode(),
6051                &terrain,
6052            )
6053            .expect("put legacy terrain");
6054        storage
6055            .put(
6056                &ChunkKey::new(pos, ChunkRecordTag::Data2D).encode(),
6057                &test_data2d_bytes(2, 24),
6058            )
6059            .expect("put conflicting old data2d");
6060        let world = BedrockWorld::from_storage_with_format(
6061            "memory",
6062            storage,
6063            OpenOptions::default(),
6064            WorldFormat::LevelDbLegacyTerrain,
6065        );
6066
6067        let (chunks, stats) = world
6068            .query_chunk_data_with_stats_blocking(
6069                [pos],
6070                ChunkLoadOptions {
6071                    data_request: exact_surface_request(
6072                        ExactSurfaceSubchunkPolicy::Full,
6073                        ExactSurfaceBiomeLoad::All,
6074                        false,
6075                    ),
6076                    threading: WorldThreadingOptions::Single,
6077                    ..ChunkLoadOptions::default()
6078                },
6079            )
6080            .expect("load conflicting legacy render chunk");
6081
6082        let sample = chunks[0]
6083            .column_sample_at(0, 0)
6084            .expect("computed column sample");
6085        assert_eq!(
6086            sample.biome,
6087            Some(TerrainColumnBiome::Legacy(LegacyBiomeSample {
6088                biome_id: 12,
6089                red: 0x34,
6090                green: 0xa8,
6091                blue: 0x53,
6092            }))
6093        );
6094        assert_eq!(stats.legacy_biome_preferred_columns, 256);
6095        assert_eq!(stats.modern_biome_fallback_columns, 0);
6096    }
6097
6098    #[test]
6099    fn modern_data2d_biome_remains_available_without_legacy_terrain() {
6100        let storage = Arc::new(MemoryStorage::new());
6101        let pos = ChunkPos {
6102            x: 0,
6103            z: 0,
6104            dimension: Dimension::Overworld,
6105        };
6106        storage
6107            .put(
6108                &ChunkKey::new(pos, ChunkRecordTag::Data2D).encode(),
6109                &test_data2d_bytes(2, 24),
6110            )
6111            .expect("put modern data2d");
6112        storage
6113            .put(
6114                &ChunkKey::subchunk(pos, 0).encode(),
6115                &test_uniform_named_subchunk_bytes("minecraft:grass_block"),
6116            )
6117            .expect("put surface subchunk");
6118        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
6119
6120        let (chunks, stats) = world
6121            .query_chunk_data_with_stats_blocking(
6122                [pos],
6123                ChunkLoadOptions {
6124                    data_request: exact_surface_request(
6125                        ExactSurfaceSubchunkPolicy::Full,
6126                        ExactSurfaceBiomeLoad::All,
6127                        false,
6128                    ),
6129                    threading: WorldThreadingOptions::Single,
6130                    ..ChunkLoadOptions::default()
6131                },
6132            )
6133            .expect("load modern render chunk");
6134
6135        let sample = chunks[0]
6136            .column_sample_at(0, 0)
6137            .expect("computed column sample");
6138        assert_eq!(sample.biome, Some(TerrainColumnBiome::Id(24)));
6139        assert_eq!(stats.legacy_biome_preferred_columns, 0);
6140        assert_eq!(stats.modern_biome_fallback_columns, 0);
6141    }
6142
6143    #[test]
6144    fn legacy_terrain_exposes_biome_colors_without_transposing_columns() {
6145        let storage = Arc::new(MemoryStorage::new());
6146        let pos = ChunkPos {
6147            x: 0,
6148            z: 0,
6149            dimension: Dimension::Overworld,
6150        };
6151        let mut terrain = test_legacy_terrain_bytes(2, 65);
6152        write_legacy_biome_sample(&mut terrain, 0, 0, 1, 0x0011_2233);
6153        write_legacy_biome_sample(&mut terrain, 15, 0, 2, 0x0044_5566);
6154        write_legacy_biome_sample(&mut terrain, 0, 15, 3, 0x0077_8899);
6155        write_legacy_biome_sample(&mut terrain, 15, 15, 4, 0x00aa_bbcc);
6156        storage
6157            .put(
6158                &ChunkKey::new(pos, ChunkRecordTag::LegacyTerrain).encode(),
6159                &terrain,
6160            )
6161            .expect("put legacy terrain");
6162        let world = BedrockWorld::from_storage_with_format(
6163            "memory",
6164            storage,
6165            OpenOptions::default(),
6166            WorldFormat::LevelDbLegacyTerrain,
6167        );
6168
6169        let chunk = world
6170            .query_chunk_data_blocking(pos, ChunkLoadOptions::default())
6171            .expect("load legacy render chunk");
6172        let colors = chunk.legacy_biome_colors.expect("legacy biome colors");
6173        let samples = chunk.legacy_biomes.expect("legacy biome samples");
6174        assert_eq!(colors[0][0], Some(0x0011_2233));
6175        assert_eq!(colors[0][15], Some(0x0044_5566));
6176        assert_eq!(colors[15][0], Some(0x0077_8899));
6177        assert_eq!(colors[15][15], Some(0x00aa_bbcc));
6178        assert_eq!(samples[0][0].map(|sample| sample.biome_id), Some(1));
6179        assert_eq!(samples[0][15].map(|sample| sample.biome_id), Some(2));
6180        assert_eq!(samples[15][0].map(|sample| sample.biome_id), Some(3));
6181        assert_eq!(samples[15][15].map(|sample| sample.biome_id), Some(4));
6182        assert_eq!(
6183            world
6184                .get_legacy_biome_color_blocking(pos, 15, 0)
6185                .expect("legacy biome color"),
6186            Some(0x0044_5566)
6187        );
6188        assert_eq!(
6189            world
6190                .get_legacy_biome_sample_blocking(pos, 15, 0)
6191                .expect("legacy biome sample")
6192                .map(|sample| (sample.biome_id, sample.rgb_u32())),
6193            Some((2, 0x0044_5566))
6194        );
6195    }
6196
6197    #[test]
6198    fn render_load_keeps_subchunks_when_legacy_terrain_is_also_present() {
6199        let storage = Arc::new(MemoryStorage::new());
6200        let pos = ChunkPos {
6201            x: 0,
6202            z: 0,
6203            dimension: Dimension::Overworld,
6204        };
6205        storage
6206            .put(
6207                &ChunkKey::new(pos, ChunkRecordTag::LegacyTerrain).encode(),
6208                &test_legacy_terrain_bytes(1, 1),
6209            )
6210            .expect("put legacy terrain");
6211        storage
6212            .put(
6213                &ChunkKey::subchunk(pos, 0).encode(),
6214                &test_surface_subchunk_bytes(),
6215            )
6216            .expect("put subchunk");
6217        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
6218
6219        let (chunks, stats) = world
6220            .query_chunk_data_with_stats_blocking(
6221                [pos],
6222                ChunkLoadOptions {
6223                    data_request: exact_surface_request(
6224                        ExactSurfaceSubchunkPolicy::Full,
6225                        ExactSurfaceBiomeLoad::TopColumns,
6226                        false,
6227                    ),
6228                    ..ChunkLoadOptions::default()
6229                },
6230            )
6231            .expect("load mixed render chunk");
6232
6233        assert_eq!(chunks.len(), 1);
6234        assert!(chunks[0].legacy_terrain.is_some());
6235        assert!(chunks[0].subchunks.contains_key(&0));
6236        assert_eq!(stats.legacy_terrain_records, 1);
6237        assert_eq!(stats.terrain_source_subchunk, 1);
6238        assert_eq!(stats.terrain_source_legacy, 0);
6239    }
6240
6241    #[test]
6242    fn exact_surface_column_samples_use_top_block_not_raw_heightmap() {
6243        let storage = Arc::new(MemoryStorage::new());
6244        let pos = ChunkPos {
6245            x: 0,
6246            z: 0,
6247            dimension: Dimension::Overworld,
6248        };
6249        storage
6250            .put(
6251                &ChunkKey::new(pos, ChunkRecordTag::Data2D).encode(),
6252                &test_data2d_bytes(1, 3),
6253            )
6254            .expect("put misleading raw height");
6255        storage
6256            .put(
6257                &ChunkKey::subchunk(pos, 0).encode(),
6258                &test_uniform_named_subchunk_bytes("minecraft:grass_block"),
6259            )
6260            .expect("put surface subchunk");
6261        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
6262
6263        let (chunks, stats) = world
6264            .query_chunk_data_with_stats_blocking(
6265                [pos],
6266                ChunkLoadOptions {
6267                    data_request: exact_surface_request(
6268                        ExactSurfaceSubchunkPolicy::Full,
6269                        ExactSurfaceBiomeLoad::TopColumns,
6270                        false,
6271                    ),
6272                    ..ChunkLoadOptions::default()
6273                },
6274            )
6275            .expect("load exact surface chunk");
6276
6277        let sample = chunks[0]
6278            .column_sample_at(0, 0)
6279            .expect("computed column sample");
6280        assert_eq!(sample.surface_y, 15);
6281        assert_eq!(sample.surface_block_state.name, "minecraft:grass_block");
6282        assert_eq!(sample.source, TerrainSampleSource::Subchunk);
6283        assert_eq!(stats.computed_surface_columns, 256);
6284        assert_eq!(stats.raw_height_mismatch_columns, 256);
6285    }
6286
6287    #[test]
6288    fn exact_surface_columns_match_full_indices() {
6289        let storage = Arc::new(MemoryStorage::new());
6290        let pos = ChunkPos {
6291            x: 0,
6292            z: 0,
6293            dimension: Dimension::Overworld,
6294        };
6295        storage
6296            .put(
6297                &ChunkKey::new(pos, ChunkRecordTag::Data2D).encode(),
6298                &test_data2d_bytes(1, 3),
6299            )
6300            .expect("put height map");
6301        storage
6302            .put(
6303                &ChunkKey::subchunk(pos, 0).encode(),
6304                &test_uniform_named_subchunk_bytes("minecraft:grass_block"),
6305            )
6306            .expect("put surface subchunk");
6307        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
6308        let surface_request = exact_surface_request(
6309            ExactSurfaceSubchunkPolicy::Full,
6310            ExactSurfaceBiomeLoad::TopColumns,
6311            false,
6312        );
6313        let full = world
6314            .query_chunk_data_blocking(
6315                pos,
6316                ChunkLoadOptions {
6317                    data_request: surface_request.clone().full_3d_indices(),
6318                    ..ChunkLoadOptions::default()
6319                },
6320            )
6321            .expect("load full indices");
6322        let surface = world
6323            .query_chunk_data_blocking(
6324                pos,
6325                ChunkLoadOptions {
6326                    data_request: surface_request,
6327                    ..ChunkLoadOptions::default()
6328                },
6329            )
6330            .expect("load surface columns");
6331
6332        assert_eq!(full.column_samples, surface.column_samples);
6333        let samples = world
6334            .load_surface_columns_blocking(
6335                pos,
6336                ChunkLoadOptions::exact_surface_columns(
6337                    ExactSurfaceSubchunkPolicy::Full,
6338                    ExactSurfaceBiomeLoad::TopColumns,
6339                    false,
6340                ),
6341            )
6342            .expect("load surface samples")
6343            .expect("surface samples");
6344        assert_eq!(surface.column_samples.as_ref(), Some(&samples));
6345    }
6346
6347    #[test]
6348    fn specialized_render_load_options_select_the_minimal_decode_contract() {
6349        let surface = ChunkLoadOptions::exact_surface_columns(
6350            ExactSurfaceSubchunkPolicy::HintThenVerify,
6351            ExactSurfaceBiomeLoad::TopColumns,
6352            false,
6353        );
6354        assert!(matches!(
6355            surface.data_request.subchunks.as_slice(),
6356            [SubchunkDataRequirement::SurfaceColumns(
6357                ExactSurfaceSubchunkPolicy::HintThenVerify
6358            )]
6359        ));
6360        assert_eq!(
6361            surface.data_request.preferred_decode_mode(),
6362            SubChunkDecodeMode::SurfaceColumns
6363        );
6364
6365        let layer = ChunkLoadOptions::layer(64);
6366        assert!(matches!(
6367            layer.data_request.subchunks.as_slice(),
6368            [SubchunkDataRequirement::Layer(64)]
6369        ));
6370        assert_eq!(
6371            layer.data_request.preferred_decode_mode(),
6372            SubChunkDecodeMode::FullIndices
6373        );
6374
6375        let height_map = ChunkLoadOptions::raw_height_map();
6376        assert!(height_map.data_request.height_map);
6377        assert_eq!(
6378            height_map.data_request.preferred_decode_mode(),
6379            SubChunkDecodeMode::CountsOnly
6380        );
6381    }
6382
6383    #[test]
6384    fn composable_map_data_request_unions_subchunk_reads_and_decoder_needs() {
6385        let pos = ChunkPos {
6386            x: 0,
6387            z: 0,
6388            dimension: Dimension::Overworld,
6389        };
6390        let request = ChunkDataRequest::new().layer(0).cave_slice(31).height_map();
6391        let options = ChunkLoadOptions::for_data_request(request.clone());
6392        let planned = planned_render_subchunk_ys(pos, &options, None).expect("plan subchunks");
6393        assert_eq!(planned.into_iter().collect::<Vec<_>>(), vec![0, 1]);
6394        assert_eq!(
6395            request.preferred_decode_mode(),
6396            SubChunkDecodeMode::FullIndices
6397        );
6398
6399        let surface = ChunkDataRequest::new()
6400            .surface_columns(ExactSurfaceSubchunkPolicy::HintThenVerify)
6401            .biome(BiomeDataRequirement::SurfaceColumns);
6402        assert_eq!(
6403            surface.preferred_decode_mode(),
6404            SubChunkDecodeMode::SurfaceColumns
6405        );
6406        assert!(!surface.height_map);
6407    }
6408
6409    #[test]
6410    fn exact_surface_samples_keep_visual_overlay_and_primary_thin_blocks() {
6411        let storage = Arc::new(MemoryStorage::new());
6412        let pos = ChunkPos {
6413            x: 0,
6414            z: 0,
6415            dimension: Dimension::Overworld,
6416        };
6417        storage
6418            .put(
6419                &ChunkKey::subchunk(pos, 0).encode(),
6420                &test_named_subchunk_bytes_with_values(
6421                    &[
6422                        "minecraft:air",
6423                        "minecraft:grass_block",
6424                        "minecraft:stone_button",
6425                        "minecraft:red_carpet",
6426                        "minecraft:snow_layer",
6427                        "minecraft:vine",
6428                    ],
6429                    |local_x, _, local_y| match (local_x, local_y) {
6430                        (_, 0) => 1,
6431                        (0, 1) => 2,
6432                        (1, 1) => 3,
6433                        (2, 1) => 4,
6434                        (3, 1) => 5,
6435                        _ => 0,
6436                    },
6437                ),
6438            )
6439            .expect("put overlay subchunk");
6440        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
6441
6442        let full = world
6443            .query_chunk_data_blocking(pos, ChunkLoadOptions::default())
6444            .expect("load exact surface chunk");
6445        let surface = world
6446            .query_chunk_data_blocking(
6447                pos,
6448                ChunkLoadOptions {
6449                    subchunk_decode: SubChunkDecodeMode::SurfaceColumns,
6450                    ..ChunkLoadOptions::default()
6451                },
6452            )
6453            .expect("load surface-column chunk");
6454        assert_eq!(full.column_samples, surface.column_samples);
6455
6456        let button = surface.column_sample_at(0, 0).expect("button column");
6457        assert_eq!(button.surface_y, 0);
6458        assert_eq!(button.surface_block_state.name, "minecraft:grass_block");
6459        assert_eq!(
6460            button
6461                .overlay
6462                .as_ref()
6463                .map(|overlay| overlay.block_state.name.as_str()),
6464            Some("minecraft:stone_button")
6465        );
6466        let carpet = surface.column_sample_at(1, 0).expect("carpet column");
6467        assert_eq!(carpet.surface_y, 1);
6468        assert_eq!(carpet.surface_block_state.name, "minecraft:red_carpet");
6469        assert!(carpet.overlay.is_none());
6470        let snow = surface.column_sample_at(2, 0).expect("snow column");
6471        assert_eq!(snow.surface_y, 1);
6472        assert_eq!(snow.surface_block_state.name, "minecraft:snow_layer");
6473        assert!(snow.overlay.is_none());
6474        let vine = surface.column_sample_at(3, 0).expect("vine column");
6475        assert_eq!(vine.surface_y, 0);
6476        assert_eq!(
6477            vine.overlay
6478                .as_ref()
6479                .map(|overlay| overlay.block_state.name.as_str()),
6480            Some("minecraft:vine")
6481        );
6482    }
6483
6484    #[test]
6485    fn exact_surface_samples_high_roof_from_secondary_storage() {
6486        let storage = Arc::new(MemoryStorage::new());
6487        let pos = ChunkPos {
6488            x: 0,
6489            z: 0,
6490            dimension: Dimension::Overworld,
6491        };
6492        storage
6493            .put(&ChunkKey::new(pos, ChunkRecordTag::Data2D).encode(), &{
6494                let mut bytes = Vec::with_capacity(768);
6495                for _ in 0..256 {
6496                    bytes.extend_from_slice(&0_i16.to_le_bytes());
6497                }
6498                bytes.extend(std::iter::repeat_n(1_u8, 256));
6499                bytes
6500            })
6501            .expect("put low raw height map");
6502        storage
6503            .put(
6504                &ChunkKey::subchunk(pos, 0).encode(),
6505                &test_named_subchunk_bytes_with_values(
6506                    &["minecraft:air", "minecraft:stone"],
6507                    |_, _, local_y| u16::from(local_y == 0),
6508                ),
6509            )
6510            .expect("put low ground subchunk");
6511        storage
6512            .put(
6513                &ChunkKey::subchunk(pos, 10).encode(),
6514                &test_named_layered_subchunk_bytes(
6515                    &["minecraft:air"],
6516                    &["minecraft:air", "minecraft:copper_block"],
6517                    |_, _, _| 0,
6518                    |_, _, local_y| u16::from(local_y == 15),
6519                ),
6520            )
6521            .expect("put high secondary-storage roof");
6522        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
6523
6524        let chunk = world
6525            .query_chunk_data_blocking(pos, ChunkLoadOptions::default())
6526            .expect("load exact surface chunk");
6527        let sample = chunk.column_sample_at(0, 0).expect("roof column");
6528
6529        assert_eq!(sample.surface_y, 175);
6530        assert_eq!(sample.surface_block_state.name, "minecraft:copper_block");
6531        assert_eq!(sample.source, TerrainSampleSource::Subchunk);
6532        assert_eq!(
6533            chunk.height_map.as_ref().expect("raw height map")[0][0],
6534            Some(0)
6535        );
6536    }
6537
6538    #[test]
6539    fn exact_surface_samples_process_secondary_storage_water_and_overlay() {
6540        let storage = Arc::new(MemoryStorage::new());
6541        let pos = ChunkPos {
6542            x: 0,
6543            z: 0,
6544            dimension: Dimension::Overworld,
6545        };
6546        storage
6547            .put(
6548                &ChunkKey::subchunk(pos, 0).encode(),
6549                &test_named_layered_subchunk_bytes(
6550                    &["minecraft:air", "minecraft:sand", "minecraft:grass_block"],
6551                    &["minecraft:air", "minecraft:water", "minecraft:stone_button"],
6552                    |local_x, _, local_y| match (local_x, local_y) {
6553                        (0, 0) => 1,
6554                        (1, 1) => 2,
6555                        _ => 0,
6556                    },
6557                    |local_x, _, local_y| match (local_x, local_y) {
6558                        (0, 0) => 1,
6559                        (1, 1) => 2,
6560                        _ => 0,
6561                    },
6562                ),
6563            )
6564            .expect("put layered water and overlay");
6565        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
6566
6567        let chunk = world
6568            .query_chunk_data_blocking(pos, ChunkLoadOptions::default())
6569            .expect("load exact surface chunk");
6570        let water = chunk.column_sample_at(0, 0).expect("water column");
6571        assert_eq!(water.surface_y, 0);
6572        assert_eq!(water.surface_block_state.name, "minecraft:water");
6573        assert_eq!(water.relief_y, 0);
6574        assert_eq!(water.relief_block_state.name, "minecraft:sand");
6575        assert_eq!(
6576            water.water.as_ref().and_then(|water| water.underwater_y),
6577            Some(0)
6578        );
6579        let overlay = chunk.column_sample_at(1, 0).expect("overlay column");
6580        assert_eq!(overlay.surface_y, 1);
6581        assert_eq!(overlay.surface_block_state.name, "minecraft:grass_block");
6582        assert_eq!(
6583            overlay
6584                .overlay
6585                .as_ref()
6586                .map(|overlay| overlay.block_state.name.as_str()),
6587            Some("minecraft:stone_button")
6588        );
6589    }
6590
6591    #[test]
6592    fn exact_surface_samples_keep_transparent_water_relief_context() {
6593        let storage = Arc::new(MemoryStorage::new());
6594        let pos = ChunkPos {
6595            x: 0,
6596            z: 0,
6597            dimension: Dimension::Overworld,
6598        };
6599        storage
6600            .put(
6601                &ChunkKey::subchunk(pos, 0).encode(),
6602                &test_named_subchunk_bytes_with_values(
6603                    &["minecraft:air", "minecraft:sand", "minecraft:water"],
6604                    |_, _, local_y| match local_y {
6605                        0 => 1,
6606                        1 | 2 => 2,
6607                        _ => 0,
6608                    },
6609                ),
6610            )
6611            .expect("put water subchunk");
6612        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
6613
6614        let chunk = world
6615            .query_chunk_data_blocking(pos, ChunkLoadOptions::default())
6616            .expect("load exact surface chunk");
6617        let sample = chunk.column_sample_at(0, 0).expect("water column");
6618        let water = sample.water.as_ref().expect("water context");
6619        assert_eq!(sample.surface_y, 2);
6620        assert_eq!(sample.surface_block_state.name, "minecraft:water");
6621        assert_eq!(sample.relief_y, 0);
6622        assert_eq!(sample.relief_block_state.name, "minecraft:sand");
6623        assert_eq!(water.depth, 2);
6624        assert_eq!(water.underwater_y, Some(0));
6625        assert_eq!(
6626            water
6627                .underwater_block_state
6628                .as_ref()
6629                .map(|state| state.name.as_str()),
6630            Some("minecraft:sand")
6631        );
6632    }
6633
6634    #[test]
6635    fn render_chunk_exact_load_preserves_legacy_subchunk_xzy_coordinates() {
6636        let storage = Arc::new(MemoryStorage::new());
6637        let pos = ChunkPos {
6638            x: 0,
6639            z: 0,
6640            dimension: Dimension::Overworld,
6641        };
6642        storage
6643            .put(
6644                &ChunkKey::subchunk(pos, 0).encode(),
6645                &test_asymmetric_legacy_subchunk_bytes(),
6646            )
6647            .expect("put legacy subchunk");
6648        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
6649
6650        let chunk = world
6651            .query_chunk_data_blocking(
6652                pos,
6653                ChunkLoadOptions {
6654                    data_request: ChunkDataRequest::new().layer(10),
6655                    ..ChunkLoadOptions::default()
6656                },
6657            )
6658            .expect("load legacy subchunk render chunk");
6659        let subchunk = chunk.subchunks.get(&0).expect("loaded legacy subchunk");
6660
6661        assert_eq!(subchunk.legacy_block_id_at(0, 10, 0), Some(1));
6662        assert_eq!(subchunk.legacy_block_id_at(15, 10, 0), Some(12));
6663        assert_eq!(subchunk.legacy_block_id_at(0, 10, 15), Some(24));
6664        assert_eq!(subchunk.legacy_block_id_at(15, 10, 15), Some(45));
6665    }
6666
6667    #[test]
6668    fn layer_query_does_not_read_surface_fallback_records() {
6669        let storage = Arc::new(MemoryStorage::new());
6670        let pos = ChunkPos {
6671            x: 0,
6672            z: 0,
6673            dimension: Dimension::Overworld,
6674        };
6675        storage
6676            .put(
6677                &ChunkKey::subchunk(pos, 0).encode(),
6678                &test_uniform_named_subchunk_bytes("minecraft:stone"),
6679            )
6680            .expect("put layer subchunk");
6681        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
6682
6683        let (chunks, stats) = world
6684            .query_chunk_data_with_stats_blocking(
6685                [pos],
6686                ChunkLoadOptions::for_data_request(ChunkDataRequest::new().layer(0)),
6687            )
6688            .expect("query fixed layer");
6689
6690        assert_eq!(chunks.len(), 1);
6691        assert!(chunks[0].subchunks.contains_key(&0));
6692        assert_eq!(stats.keys_requested, 1);
6693        assert_eq!(stats.keys_found, 1);
6694        assert_eq!(stats.legacy_terrain_records, 0);
6695    }
6696
6697    #[test]
6698    fn chunk_query_defaults_to_reusing_storage_blocks() {
6699        assert_eq!(
6700            ChunkLoadOptions::default().storage_cache_policy,
6701            StorageCachePolicy::Use
6702        );
6703    }
6704
6705    #[test]
6706    fn decode_timing_preserves_sub_millisecond_samples() {
6707        let mut total = ChunkDecodeTiming::default();
6708        total.add(ChunkDecodeTiming {
6709            biome_parse_us: 125,
6710            subchunk_parse_us: 250,
6711            surface_scan_us: 375,
6712            block_entity_parse_us: 500,
6713        });
6714        total.add(ChunkDecodeTiming {
6715            biome_parse_us: 875,
6716            subchunk_parse_us: 750,
6717            surface_scan_us: 625,
6718            block_entity_parse_us: 500,
6719        });
6720
6721        assert_eq!(total.biome_parse_us, 1_000);
6722        assert_eq!(total.subchunk_parse_us, 1_000);
6723        assert_eq!(total.surface_scan_us, 1_000);
6724        assert_eq!(total.block_entity_parse_us, 1_000);
6725    }
6726
6727    #[test]
6728    #[allow(clippy::similar_names)]
6729    fn render_chunk_exact_batch_keeps_shuffled_positions_bound_to_records() {
6730        let storage = Arc::new(MemoryStorage::new());
6731        let fixtures = [
6732            (
6733                ChunkPos {
6734                    x: -3,
6735                    z: 1,
6736                    dimension: Dimension::Overworld,
6737                },
6738                "minecraft:signature_a",
6739            ),
6740            (
6741                ChunkPos {
6742                    x: 2,
6743                    z: -4,
6744                    dimension: Dimension::Overworld,
6745                },
6746                "minecraft:signature_b",
6747            ),
6748            (
6749                ChunkPos {
6750                    x: 0,
6751                    z: 0,
6752                    dimension: Dimension::Overworld,
6753                },
6754                "minecraft:signature_c",
6755            ),
6756        ];
6757        for (pos, block_name) in fixtures.iter().copied() {
6758            storage
6759                .put(
6760                    &ChunkKey::subchunk(pos, 4).encode(),
6761                    &test_uniform_named_subchunk_bytes(block_name),
6762                )
6763                .expect("put named subchunk");
6764        }
6765        let world = BedrockWorld::from_storage("memory", storage, OpenOptions::default());
6766
6767        let (chunks, stats) = world
6768            .query_chunk_data_with_stats_blocking(
6769                vec![fixtures[1].0, fixtures[0].0, fixtures[2].0, fixtures[1].0],
6770                ChunkLoadOptions {
6771                    data_request: ChunkDataRequest::new().layer(64),
6772                    threading: WorldThreadingOptions::Fixed(4),
6773                    priority: ChunkLoadPriority::DistanceFrom {
6774                        chunk_x: 0,
6775                        chunk_z: 0,
6776                    },
6777                    ..ChunkLoadOptions::default()
6778                },
6779            )
6780            .expect("load shuffled render chunks");
6781
6782        assert_eq!(chunks.len(), 4);
6783        assert_eq!(stats.prefix_scans, 0);
6784        assert!(stats.exact_get_batches > 0);
6785        for chunk in chunks {
6786            let expected = fixtures
6787                .iter()
6788                .find_map(|(pos, block_name)| (*pos == chunk.pos).then_some(*block_name))
6789                .expect("known chunk position");
6790            let subchunk = chunk.subchunks.get(&4).expect("loaded subchunk");
6791            let state = subchunk
6792                .block_state_at(0, 0, 0)
6793                .expect("decoded signature block");
6794            assert_eq!(state.name, expected, "chunk {:?}", chunk.pos);
6795        }
6796    }
6797
6798    fn test_surface_subchunk_bytes() -> Vec<u8> {
6799        let palette = ["minecraft:air", "minecraft:sand", "minecraft:water"];
6800        let mut bytes = vec![8, 1, 2 << 1];
6801        let values_per_word = 16_usize;
6802        let mut words = vec![0_u32; 256];
6803        for local_z in 0..16_u8 {
6804            for local_x in 0..16_u8 {
6805                for (local_y, value) in [(0_u8, 1_u32), (1, 2)] {
6806                    let block_index = block_storage_index(local_x, local_y, local_z);
6807                    let word_index = block_index / values_per_word;
6808                    let bit_offset = (block_index % values_per_word) * 2;
6809                    words[word_index] |= value << bit_offset;
6810                }
6811            }
6812        }
6813        for word in words {
6814            bytes.extend_from_slice(&word.to_le_bytes());
6815        }
6816        bytes.extend_from_slice(&(palette.len() as i32).to_le_bytes());
6817        for name in palette {
6818            let tag = NbtTag::Compound(IndexMap::from([
6819                ("name".to_string(), NbtTag::String(name.to_string())),
6820                ("states".to_string(), NbtTag::Compound(IndexMap::new())),
6821                ("version".to_string(), NbtTag::Int(1)),
6822            ]));
6823            bytes.extend_from_slice(&crate::nbt::serialize_root_nbt(&tag).expect("nbt"));
6824        }
6825        bytes
6826    }
6827
6828    fn test_uniform_named_subchunk_bytes(block_name: &str) -> Vec<u8> {
6829        let palette = ["minecraft:air", block_name];
6830        let mut bytes = vec![8, 1, 1 << 1];
6831        let mut words = vec![0_u32; 128];
6832        for local_z in 0..16_u8 {
6833            for local_x in 0..16_u8 {
6834                for local_y in 0..16_u8 {
6835                    let block_index = block_storage_index(local_x, local_y, local_z);
6836                    let word_index = block_index / 32;
6837                    let bit_offset = block_index % 32;
6838                    words[word_index] |= 1_u32 << bit_offset;
6839                }
6840            }
6841        }
6842        for word in words {
6843            bytes.extend_from_slice(&word.to_le_bytes());
6844        }
6845        bytes.extend_from_slice(&(palette.len() as i32).to_le_bytes());
6846        for name in palette {
6847            let tag = NbtTag::Compound(IndexMap::from([
6848                ("name".to_string(), NbtTag::String(name.to_string())),
6849                ("states".to_string(), NbtTag::Compound(IndexMap::new())),
6850                ("version".to_string(), NbtTag::Int(1)),
6851            ]));
6852            bytes.extend_from_slice(&crate::nbt::serialize_root_nbt(&tag).expect("nbt"));
6853        }
6854        bytes
6855    }
6856
6857    fn test_named_subchunk_bytes_with_values(
6858        palette: &[&str],
6859        value_at: impl Fn(u8, u8, u8) -> u16,
6860    ) -> Vec<u8> {
6861        let bits_per_value = match palette.len() {
6862            0..=2 => 1_u8,
6863            3..=4 => 2_u8,
6864            5..=16 => 4_u8,
6865            _ => 8_u8,
6866        };
6867        let values_per_word = usize::from(32 / bits_per_value);
6868        let word_count = 4096_usize.div_ceil(values_per_word);
6869        let mut bytes = vec![8, 1, bits_per_value << 1];
6870        let mut words = vec![0_u32; word_count];
6871        for local_z in 0..16_u8 {
6872            for local_x in 0..16_u8 {
6873                for local_y in 0..16_u8 {
6874                    let value = value_at(local_x, local_z, local_y);
6875                    if value == 0 {
6876                        continue;
6877                    }
6878                    let block_index = block_storage_index(local_x, local_y, local_z);
6879                    let word_index = block_index / values_per_word;
6880                    let bit_offset = (block_index % values_per_word) * usize::from(bits_per_value);
6881                    words[word_index] |= u32::from(value) << bit_offset;
6882                }
6883            }
6884        }
6885        for word in words {
6886            bytes.extend_from_slice(&word.to_le_bytes());
6887        }
6888        bytes.extend_from_slice(&(palette.len() as i32).to_le_bytes());
6889        for name in palette {
6890            let tag = NbtTag::Compound(IndexMap::from([
6891                ("name".to_string(), NbtTag::String((*name).to_string())),
6892                ("states".to_string(), NbtTag::Compound(IndexMap::new())),
6893                ("version".to_string(), NbtTag::Int(1)),
6894            ]));
6895            bytes.extend_from_slice(&crate::nbt::serialize_root_nbt(&tag).expect("nbt"));
6896        }
6897        bytes
6898    }
6899
6900    fn test_named_layered_subchunk_bytes(
6901        lower_palette: &[&str],
6902        upper_palette: &[&str],
6903        lower_value_at: impl Fn(u8, u8, u8) -> u16,
6904        upper_value_at: impl Fn(u8, u8, u8) -> u16,
6905    ) -> Vec<u8> {
6906        let mut bytes = vec![8, 2];
6907        append_named_palette_storage(&mut bytes, lower_palette, lower_value_at);
6908        append_named_palette_storage(&mut bytes, upper_palette, upper_value_at);
6909        bytes
6910    }
6911
6912    fn append_named_palette_storage(
6913        bytes: &mut Vec<u8>,
6914        palette: &[&str],
6915        value_at: impl Fn(u8, u8, u8) -> u16,
6916    ) {
6917        let bits_per_value = match palette.len() {
6918            0..=2 => 1_u8,
6919            3..=4 => 2_u8,
6920            5..=16 => 4_u8,
6921            _ => 8_u8,
6922        };
6923        let values_per_word = usize::from(32 / bits_per_value);
6924        let word_count = 4096_usize.div_ceil(values_per_word);
6925        let mut words = vec![0_u32; word_count];
6926        for local_z in 0..16_u8 {
6927            for local_x in 0..16_u8 {
6928                for local_y in 0..16_u8 {
6929                    let value = value_at(local_x, local_z, local_y);
6930                    if value == 0 {
6931                        continue;
6932                    }
6933                    let block_index = block_storage_index(local_x, local_y, local_z);
6934                    let word_index = block_index / values_per_word;
6935                    let bit_offset = (block_index % values_per_word) * usize::from(bits_per_value);
6936                    words[word_index] |= u32::from(value) << bit_offset;
6937                }
6938            }
6939        }
6940        bytes.push(bits_per_value << 1);
6941        for word in words {
6942            bytes.extend_from_slice(&word.to_le_bytes());
6943        }
6944        bytes.extend_from_slice(&(palette.len() as i32).to_le_bytes());
6945        for name in palette {
6946            let tag = NbtTag::Compound(IndexMap::from([
6947                ("name".to_string(), NbtTag::String((*name).to_string())),
6948                ("states".to_string(), NbtTag::Compound(IndexMap::new())),
6949                ("version".to_string(), NbtTag::Int(1)),
6950            ]));
6951            bytes.extend_from_slice(&crate::nbt::serialize_root_nbt(&tag).expect("nbt"));
6952        }
6953    }
6954
6955    fn test_asymmetric_legacy_subchunk_bytes() -> Vec<u8> {
6956        let mut bytes = vec![0_u8; crate::LEGACY_SUBCHUNK_WITH_LIGHT_VALUE_LEN];
6957        bytes[0] = 2;
6958        for local_z in 0..16_u8 {
6959            for local_x in 0..16_u8 {
6960                let block_id = match (local_x >= 8, local_z >= 8) {
6961                    (false, false) => 1,
6962                    (true, false) => 12,
6963                    (false, true) => 24,
6964                    (true, true) => 45,
6965                };
6966                let index = crate::LegacySubChunk::block_index(local_x, 10, local_z)
6967                    .expect("legacy subchunk index");
6968                bytes[1 + index] = block_id;
6969            }
6970        }
6971        bytes
6972    }
6973
6974    fn test_data2d_bytes(height: i16, biome: u8) -> Vec<u8> {
6975        let mut bytes = Vec::with_capacity(768);
6976        for _ in 0..256 {
6977            bytes.extend_from_slice(&height.to_le_bytes());
6978        }
6979        bytes.extend(std::iter::repeat_n(biome, 256));
6980        bytes
6981    }
6982
6983    fn test_data3d_height_bytes(height: i16) -> Vec<u8> {
6984        let mut bytes = Vec::with_capacity(512);
6985        for _ in 0..256 {
6986            bytes.extend_from_slice(&height.to_le_bytes());
6987        }
6988        bytes
6989    }
6990
6991    fn test_asymmetric_data2d_bytes() -> Vec<u8> {
6992        let mut bytes = Vec::with_capacity(768);
6993        for local_z in 0..16_i16 {
6994            for local_x in 0..16_i16 {
6995                let height = 100 + local_x * 10 + local_z;
6996                bytes.extend_from_slice(&height.to_le_bytes());
6997            }
6998        }
6999        for local_z in 0..16_u8 {
7000            for local_x in 0..16_u8 {
7001                bytes.push(local_x * 10 + local_z);
7002            }
7003        }
7004        bytes
7005    }
7006
7007    fn test_legacy_terrain_bytes(block_id: u8, height: u8) -> Vec<u8> {
7008        let mut bytes = vec![0_u8; crate::LEGACY_TERRAIN_VALUE_LEN];
7009        for local_z in 0..16_u8 {
7010            for local_x in 0..16_u8 {
7011                for local_y in 0..=height.min(127) {
7012                    let index = crate::LegacyTerrain::block_index(local_x, local_y, local_z)
7013                        .expect("legacy block index");
7014                    bytes[index] = block_id;
7015                }
7016                bytes[crate::LEGACY_TERRAIN_BLOCK_COUNT
7017                    + crate::LEGACY_TERRAIN_BLOCK_COUNT / 2 * 3
7018                    + raw_2d_column_index(local_x, local_z)] = height;
7019            }
7020        }
7021        bytes
7022    }
7023
7024    fn write_legacy_biome_sample(
7025        bytes: &mut [u8],
7026        local_x: u8,
7027        local_z: u8,
7028        biome_id: u8,
7029        color: u32,
7030    ) {
7031        let offset = crate::LEGACY_TERRAIN_BLOCK_COUNT
7032            + crate::LEGACY_TERRAIN_BLOCK_COUNT / 2 * 3
7033            + 16 * 16
7034            + raw_2d_column_index(local_x, local_z) * 4;
7035        bytes[offset] = biome_id;
7036        bytes[offset + 1] = ((color >> 16) & 0xff) as u8;
7037        bytes[offset + 2] = ((color >> 8) & 0xff) as u8;
7038        bytes[offset + 3] = (color & 0xff) as u8;
7039    }
7040
7041    fn raw_2d_column_index(local_x: u8, local_z: u8) -> usize {
7042        usize::from(local_z) * 16 + usize::from(local_x)
7043    }
7044}
7045
7046fn validate_local_column(local_x: u8, local_z: u8) -> Result<()> {
7047    if local_x >= 16 || local_z >= 16 {
7048        return Err(BedrockWorldError::Validation(format!(
7049            "local biome coordinates must be 0..15, got x={local_x}, z={local_z}"
7050        )));
7051    }
7052    Ok(())
7053}
7054
7055fn insert_needed_surface_subchunks(
7056    subchunk_ys: &mut BTreeSet<i8>,
7057    height_map: Option<&[[Option<i16>; 16]; 16]>,
7058    min_subchunk_y: i8,
7059    max_subchunk_y: i8,
7060) {
7061    const SURFACE_LOOKDOWN_SUBCHUNKS: i8 = 6;
7062    const SURFACE_LOOKUP_SUBCHUNKS: i8 = 4;
7063    let Some(height_map) = height_map else {
7064        return;
7065    };
7066    for row in height_map {
7067        for height in row.iter().flatten() {
7068            if let Ok(surface_y) = block_y_to_subchunk_y(i32::from(*height)) {
7069                let lower_y = surface_y
7070                    .saturating_sub(SURFACE_LOOKDOWN_SUBCHUNKS)
7071                    .max(min_subchunk_y);
7072                let upper_y = surface_y
7073                    .saturating_add(SURFACE_LOOKUP_SUBCHUNKS)
7074                    .clamp(min_subchunk_y, max_subchunk_y);
7075                for subchunk_y in lower_y..=upper_y {
7076                    subchunk_ys.insert(subchunk_y);
7077                }
7078            }
7079        }
7080    }
7081}
7082
7083fn block_y_to_subchunk_y(y: i32) -> Result<i8> {
7084    let subchunk_y = y.div_euclid(16);
7085    i8::try_from(subchunk_y).map_err(|_| {
7086        BedrockWorldError::Validation(format!(
7087            "block y={y} cannot be represented as a Bedrock subchunk index"
7088        ))
7089    })
7090}
7091
7092fn biome_storage_contains_y(storage: &ParsedBiomeStorage, y: i32) -> bool {
7093    storage
7094        .y
7095        .is_none_or(|start_y| (start_y..start_y + 16).contains(&y))
7096}
7097
7098fn biome_storage_bucket_y(y: i32) -> i32 {
7099    y.div_euclid(16) * 16
7100}
7101
7102fn biome_id_from_storage(
7103    storage: &ParsedBiomeStorage,
7104    local_x: u8,
7105    local_z: u8,
7106    y: i32,
7107) -> Option<u32> {
7108    let local_y = if let Some(start_y) = storage.y {
7109        u8::try_from(y - start_y).ok()?
7110    } else {
7111        0
7112    };
7113    storage.biome_id_at(local_x, local_y, local_z)
7114}
7115
7116fn height_map_index(local_x: u8, local_z: u8) -> usize {
7117    usize::from(local_z) * 16 + usize::from(local_x)
7118}
7119
7120fn column_index(local_x: u8, local_z: u8) -> Option<usize> {
7121    (local_x < 16 && local_z < 16).then_some(height_map_index(local_x, local_z))
7122}
7123
7124fn raw_height_at(
7125    height_map: Option<&[[Option<i16>; 16]; 16]>,
7126    local_x: u8,
7127    local_z: u8,
7128) -> Option<i16> {
7129    height_map?[usize::from(local_z)][usize::from(local_x)]
7130}
7131
7132fn raw_height_mismatch_columns(chunk: &ChunkData) -> usize {
7133    let Some(samples) = chunk.column_samples.as_ref() else {
7134        return 0;
7135    };
7136    let Some(height_map) = chunk.height_map.as_ref() else {
7137        return 0;
7138    };
7139    let mut mismatches = 0usize;
7140    for local_z in 0..16_u8 {
7141        for local_x in 0..16_u8 {
7142            if let Some(sample) = samples.get(local_x, local_z) {
7143                if height_map[usize::from(local_z)][usize::from(local_x)]
7144                    .is_some_and(|raw_height| raw_height != sample.surface_y)
7145                {
7146                    mismatches = mismatches.saturating_add(1);
7147                }
7148            }
7149        }
7150    }
7151    mismatches
7152}
7153
7154fn missing_surface_columns(chunk: &ChunkData) -> usize {
7155    chunk.column_samples.as_ref().map_or(0, |samples| {
7156        256usize.saturating_sub(samples.sampled_columns())
7157    })
7158}
7159
7160fn needed_exact_surface_chunk_requires_full_reload(chunk: &ChunkData) -> Result<bool> {
7161    let Some(samples) = chunk.column_samples.as_ref() else {
7162        return Ok(false);
7163    };
7164    if samples.sampled_columns() < 16 * 16 {
7165        return Ok(true);
7166    }
7167    if raw_height_mismatch_columns(chunk) > 0 {
7168        return Ok(true);
7169    }
7170    let Some(loaded_max_subchunk_y) = chunk.subchunks.keys().next_back().copied() else {
7171        return Ok(true);
7172    };
7173    let (_, world_max_subchunk_y) = chunk.pos.subchunk_index_range(chunk.version);
7174    if loaded_max_subchunk_y >= world_max_subchunk_y {
7175        return Ok(false);
7176    }
7177    for sample in samples.iter() {
7178        if block_y_to_subchunk_y(i32::from(sample.surface_y))? == loaded_max_subchunk_y {
7179            return Ok(true);
7180        }
7181        if let Some(overlay) = sample.overlay.as_ref() {
7182            if block_y_to_subchunk_y(i32::from(overlay.y))? == loaded_max_subchunk_y {
7183                return Ok(true);
7184            }
7185        }
7186    }
7187    Ok(false)
7188}
7189
7190fn legacy_world_block_state(id: u8, data: u8) -> BlockState {
7191    let mut states = BTreeMap::new();
7192    states.insert("data".to_string(), NbtTag::Byte(data as i8));
7193    BlockState {
7194        name: legacy_world_block_name(id, data),
7195        states,
7196        version: None,
7197    }
7198}
7199
7200#[allow(clippy::too_many_lines)]
7201fn legacy_world_block_name(id: u8, data: u8) -> String {
7202    let name = match id {
7203        0 => "minecraft:air",
7204        1 => match data & 0x7 {
7205            1 => "minecraft:granite",
7206            2 => "minecraft:polished_granite",
7207            3 => "minecraft:diorite",
7208            4 => "minecraft:polished_diorite",
7209            5 => "minecraft:andesite",
7210            6 => "minecraft:polished_andesite",
7211            _ => "minecraft:stone",
7212        },
7213        2 => "minecraft:grass_block",
7214        3 => match data & 0x3 {
7215            1 => "minecraft:coarse_dirt",
7216            2 => "minecraft:podzol",
7217            _ => "minecraft:dirt",
7218        },
7219        4 => "minecraft:cobblestone",
7220        5 => legacy_world_wood_name(data, "planks"),
7221        6 => "minecraft:oak_sapling",
7222        7 => "minecraft:bedrock",
7223        8 | 9 => "minecraft:water",
7224        10 | 11 => "minecraft:lava",
7225        12 => match data & 0x1 {
7226            1 => "minecraft:red_sand",
7227            _ => "minecraft:sand",
7228        },
7229        13 => "minecraft:gravel",
7230        14 => "minecraft:gold_ore",
7231        15 => "minecraft:iron_ore",
7232        16 => "minecraft:coal_ore",
7233        17 => legacy_world_wood_name(data, "log"),
7234        18 => legacy_world_wood_name(data, "leaves"),
7235        19 => "minecraft:sponge",
7236        20 => "minecraft:glass",
7237        21 => "minecraft:lapis_ore",
7238        22 => "minecraft:lapis_block",
7239        24 => "minecraft:sandstone",
7240        26 => "minecraft:bed",
7241        30 => "minecraft:cobweb",
7242        31 => match data {
7243            1 => "minecraft:short_grass",
7244            2 => "minecraft:fern",
7245            _ => "minecraft:dead_bush",
7246        },
7247        32 => "minecraft:dead_bush",
7248        35 => legacy_world_wool_name(data),
7249        37 => "minecraft:dandelion",
7250        38 => "minecraft:poppy",
7251        39 => "minecraft:brown_mushroom",
7252        40 => "minecraft:red_mushroom",
7253        41 => "minecraft:gold_block",
7254        42 => "minecraft:iron_block",
7255        43 | 44 => "minecraft:stone_slab",
7256        45 => "minecraft:bricks",
7257        46 => "minecraft:tnt",
7258        47 => "minecraft:bookshelf",
7259        48 => "minecraft:mossy_cobblestone",
7260        49 => "minecraft:obsidian",
7261        50 => "minecraft:torch",
7262        51 => "minecraft:fire",
7263        52 => "minecraft:spawner",
7264        53 => "minecraft:oak_stairs",
7265        54 => "minecraft:chest",
7266        56 => "minecraft:diamond_ore",
7267        57 => "minecraft:diamond_block",
7268        58 => "minecraft:crafting_table",
7269        59 => "minecraft:wheat",
7270        60 => "minecraft:farmland",
7271        61 | 62 => "minecraft:furnace",
7272        63 | 68 => "minecraft:oak_sign",
7273        64 => "minecraft:oak_door",
7274        65 => "minecraft:ladder",
7275        66 => "minecraft:rail",
7276        67 => "minecraft:cobblestone_stairs",
7277        71 => "minecraft:iron_door",
7278        73 | 74 => "minecraft:redstone_ore",
7279        78 => "minecraft:snow",
7280        79 => "minecraft:ice",
7281        80 => "minecraft:snow_block",
7282        81 => "minecraft:cactus",
7283        82 => "minecraft:clay",
7284        83 => "minecraft:sugar_cane",
7285        85 => "minecraft:oak_fence",
7286        86 => "minecraft:pumpkin",
7287        87 => "minecraft:netherrack",
7288        88 => "minecraft:soul_sand",
7289        89 => "minecraft:glowstone",
7290        91 => "minecraft:jack_o_lantern",
7291        95 => "minecraft:invisible_bedrock",
7292        98 => "minecraft:stone_bricks",
7293        99 | 100 => "minecraft:mushroom_stem",
7294        103 => "minecraft:melon",
7295        106 => "minecraft:vine",
7296        107 => "minecraft:oak_fence_gate",
7297        108 => "minecraft:brick_stairs",
7298        109 => "minecraft:stone_brick_stairs",
7299        110 => "minecraft:mycelium",
7300        111 => "minecraft:lily_pad",
7301        112 => "minecraft:nether_bricks",
7302        121 => "minecraft:end_stone",
7303        129 => "minecraft:emerald_ore",
7304        133 => "minecraft:emerald_block",
7305        155 => "minecraft:quartz_block",
7306        159 | 172 => "minecraft:terracotta",
7307        161 => legacy_world_wood_name(data.saturating_add(4), "leaves"),
7308        162 => legacy_world_wood_name(data.saturating_add(4), "log"),
7309        169 => "minecraft:sea_lantern",
7310        170 => "minecraft:hay_block",
7311        171 => "minecraft:white_carpet",
7312        173 => "minecraft:coal_block",
7313        174 => "minecraft:packed_ice",
7314        175 => "minecraft:sunflower",
7315        _ => return format!("legacy:{id}"),
7316    };
7317    name.to_string()
7318}
7319
7320fn legacy_world_wood_name(data: u8, suffix: &'static str) -> &'static str {
7321    match (data & 0x7, suffix) {
7322        (1, "planks") => "minecraft:spruce_planks",
7323        (2, "planks") => "minecraft:birch_planks",
7324        (3, "planks") => "minecraft:jungle_planks",
7325        (4, "planks") => "minecraft:acacia_planks",
7326        (5, "planks") => "minecraft:dark_oak_planks",
7327        (_, "planks") => "minecraft:oak_planks",
7328        (1, "log") => "minecraft:spruce_log",
7329        (2, "log") => "minecraft:birch_log",
7330        (3, "log") => "minecraft:jungle_log",
7331        (4, "log") => "minecraft:acacia_log",
7332        (5, "log") => "minecraft:dark_oak_log",
7333        (_, "log") => "minecraft:oak_log",
7334        (1, "leaves") => "minecraft:spruce_leaves",
7335        (2, "leaves") => "minecraft:birch_leaves",
7336        (3, "leaves") => "minecraft:jungle_leaves",
7337        (4, "leaves") => "minecraft:acacia_leaves",
7338        (5, "leaves") => "minecraft:dark_oak_leaves",
7339        _ => "minecraft:oak_leaves",
7340    }
7341}
7342
7343fn legacy_world_wool_name(data: u8) -> &'static str {
7344    match data & 0x0f {
7345        1 => "minecraft:orange_wool",
7346        2 => "minecraft:magenta_wool",
7347        3 => "minecraft:light_blue_wool",
7348        4 => "minecraft:yellow_wool",
7349        5 => "minecraft:lime_wool",
7350        6 => "minecraft:pink_wool",
7351        7 => "minecraft:gray_wool",
7352        8 => "minecraft:light_gray_wool",
7353        9 => "minecraft:cyan_wool",
7354        10 => "minecraft:purple_wool",
7355        11 => "minecraft:blue_wool",
7356        12 => "minecraft:brown_wool",
7357        13 => "minecraft:green_wool",
7358        14 => "minecraft:red_wool",
7359        15 => "minecraft:black_wool",
7360        _ => "minecraft:white_wool",
7361    }
7362}