1use 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#[derive(Debug, Clone)]
52pub struct OpenOptions {
53 pub read_only: bool,
55 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)]
70pub enum WorldFormatHint {
72 #[default]
73 Auto,
75 LevelDb,
77 PocketChunksDat,
79}
80
81#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
82pub enum WorldFormat {
84 #[default]
85 LevelDb,
87 LevelDbLegacyTerrain,
89 PocketChunksDat,
91}
92
93pub struct BedrockWorld<S = Arc<dyn WorldStorage>> {
98 path: PathBuf,
99 options: OpenOptions,
100 storage: S,
101 format: WorldFormat,
102}
103
104pub trait WorldStorageHandle: Clone + Send + Sync + 'static {
106 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)]
135pub struct SurfaceColumnOptions {
137 pub skip_air: bool,
139 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)]
153pub struct SurfaceColumn {
155 pub y: i32,
157 pub block_name: String,
159 pub biome_id: Option<u32>,
161 pub water_depth: u8,
163 pub under_water_block_name: Option<String>,
165 pub is_fallback: bool,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum ExactSurfaceSubchunkPolicy {
172 Full,
174 HintThenVerify,
176}
177
178#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
179pub struct WorldPipelineOptions {
181 pub queue_depth: usize,
183 pub chunk_batch_size: usize,
185 pub subchunk_decode_workers: usize,
187 pub progress_interval: usize,
189}
190
191impl WorldPipelineOptions {
192 #[must_use]
193 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 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)]
216pub enum ChunkLoadPriority {
218 #[default]
219 RowMajor,
221 DistanceFrom {
223 chunk_x: i32,
225 chunk_z: i32,
227 },
228}
229
230#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
231pub enum ExactSurfaceBiomeLoad {
233 None,
235 #[default]
236 TopColumns,
238 All,
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub enum SubchunkDataRequirement {
245 SurfaceColumns(ExactSurfaceSubchunkPolicy),
247 Layer(i32),
249 CaveSlice(i32),
251 Full3dIndices,
253}
254
255#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
256pub enum BiomeDataRequirement {
258 #[default]
260 None,
261 SurfaceColumns,
263 Layer(i32),
265 All,
267}
268
269#[derive(Debug, Clone, Default, PartialEq, Eq)]
270pub struct ChunkDataRequest {
275 pub subchunks: Vec<SubchunkDataRequirement>,
277 pub height_map: bool,
279 pub biome: BiomeDataRequirement,
281 pub block_entities: bool,
283}
284
285impl ChunkDataRequest {
286 #[must_use]
287 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 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 pub fn layer(mut self, y: i32) -> Self {
307 self.push_subchunk_requirement(SubchunkDataRequirement::Layer(y));
308 self
309 }
310
311 #[must_use]
312 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 pub fn full_3d_indices(mut self) -> Self {
321 self.push_subchunk_requirement(SubchunkDataRequirement::Full3dIndices);
322 self
323 }
324
325 #[must_use]
326 pub const fn height_map(mut self) -> Self {
328 self.height_map = true;
329 self
330 }
331
332 #[must_use]
333 pub const fn biome(mut self, biome: BiomeDataRequirement) -> Self {
335 self.biome = biome;
336 self
337 }
338
339 #[must_use]
340 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)]
354pub enum TerrainSampleSource {
356 Subchunk,
358 LegacyTerrain,
360 LegacyFallback,
362}
363
364#[derive(Debug, Clone, Copy, PartialEq, Eq)]
365pub enum TerrainColumnBiome {
367 Id(u32),
369 Legacy(LegacyBiomeSample),
371}
372
373#[derive(Debug, Clone, PartialEq)]
374pub struct TerrainColumnOverlay {
376 pub y: i16,
378 pub block_state: BlockState,
380 pub source: TerrainSampleSource,
382}
383
384#[derive(Debug, Clone, PartialEq)]
385pub struct TerrainColumnWater {
387 pub surface_y: i16,
389 pub block_state: BlockState,
391 pub depth: u8,
393 pub underwater_y: Option<i16>,
395 pub underwater_block_state: Option<BlockState>,
397 pub source: TerrainSampleSource,
399}
400
401#[derive(Debug, Clone, PartialEq)]
402pub struct TerrainColumnSample {
404 pub surface_y: i16,
406 pub surface_block_state: BlockState,
408 pub relief_y: i16,
410 pub relief_block_state: BlockState,
412 pub overlay: Option<TerrainColumnOverlay>,
414 pub water: Option<TerrainColumnWater>,
416 pub biome: Option<TerrainColumnBiome>,
418 pub source: TerrainSampleSource,
420}
421
422#[derive(Debug, Clone, PartialEq)]
423pub struct TerrainColumnSamples {
425 columns: Vec<Option<TerrainColumnSample>>,
426}
427
428impl TerrainColumnSamples {
429 #[must_use]
430 pub fn new() -> Self {
432 Self {
433 columns: vec![None; 16 * 16],
434 }
435 }
436
437 #[must_use]
438 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 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 pub fn sampled_columns(&self) -> usize {
457 self.columns
458 .iter()
459 .filter(|sample| sample.is_some())
460 .count()
461 }
462
463 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)]
476pub struct ChunkLoadStats {
478 pub requested_chunks: usize,
480 pub loaded_chunks: usize,
482 pub subchunks_decoded: usize,
484 pub worker_threads: usize,
486 pub queue_wait_ms: u128,
488 pub load_ms: u128,
490 pub keys_requested: usize,
492 pub keys_found: usize,
494 pub exact_get_batches: usize,
496 pub prefix_scans: usize,
498 pub decode_ms: u128,
500 pub db_read_ms: u128,
502 pub biome_parse_ms: u128,
504 pub biome_parse_us: u128,
506 pub subchunk_parse_ms: u128,
508 pub subchunk_parse_us: u128,
510 pub surface_scan_ms: u128,
512 pub surface_scan_us: u128,
514 pub block_entity_parse_ms: u128,
516 pub block_entity_parse_us: u128,
518 pub full_reload_ms: u128,
520 pub legacy_terrain_records: usize,
522 pub legacy_biome_samples: usize,
524 pub legacy_biome_colors: usize,
526 pub terrain_source_legacy: usize,
528 pub terrain_source_subchunk: usize,
530 pub legacy_pocket_chunks: usize,
532 pub detected_format: WorldFormat,
534 pub computed_surface_columns: usize,
536 pub raw_height_mismatch_columns: usize,
538 pub missing_subchunk_columns: usize,
540 pub legacy_fallback_columns: usize,
542 pub legacy_biome_preferred_columns: usize,
544 pub modern_biome_fallback_columns: usize,
546}
547
548#[derive(Debug, Clone)]
549pub struct ChunkLoadOptions {
551 pub data_request: ChunkDataRequest,
553 pub subchunk_decode: SubChunkDecodeMode,
555 pub threading: WorldThreadingOptions,
557 pub pipeline: WorldPipelineOptions,
559 pub cancel: Option<CancelFlag>,
561 pub progress: Option<ProgressSink>,
563 pub priority: ChunkLoadPriority,
565 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 pub fn for_data_request(data_request: ChunkDataRequest) -> Self {
590 Self {
591 data_request,
592 ..Self::default()
593 }
594 }
595
596 #[must_use]
597 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 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 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 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)]
649pub struct ChunkBlockEntity {
651 pub id: Option<String>,
653 pub position: Option<[i32; 3]>,
655 pub nbt: NbtTag,
657}
658
659#[derive(Debug, Clone, PartialEq)]
660pub struct ChunkData {
662 pub pos: ChunkPos,
664 pub is_loaded: bool,
666 pub height_map: Option<[[Option<i16>; 16]; 16]>,
668 pub legacy_biomes: Option<[[Option<LegacyBiomeSample>; 16]; 16]>,
670 pub legacy_biome_colors: Option<[[Option<u32>; 16]; 16]>,
672 pub biome_data: BTreeMap<i32, ParsedBiomeStorage>,
674 pub subchunks: BTreeMap<i8, SubChunk>,
676 pub block_entities: Vec<ChunkBlockEntity>,
678 pub legacy_terrain: Option<LegacyTerrain>,
680 pub column_samples: Option<TerrainColumnSamples>,
682 pub version: crate::ChunkVersion,
684}
685
686impl ChunkData {
687 #[must_use]
688 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)]
742pub struct WorldChunkQueryRegionLoadOptions {
744 pub data_request: ChunkDataRequest,
746 pub subchunk_decode: SubChunkDecodeMode,
748 pub threading: WorldThreadingOptions,
750 pub pipeline: WorldPipelineOptions,
752 pub cancel: Option<CancelFlag>,
754 pub progress: Option<ProgressSink>,
756 pub priority: ChunkLoadPriority,
758 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)]
821pub struct WorldChunkQueryRegion {
823 pub dimension: crate::Dimension,
825 pub min_chunk_x: i32,
827 pub min_chunk_z: i32,
829 pub max_chunk_x: i32,
831 pub max_chunk_z: i32,
833}
834
835#[derive(Debug, Clone, PartialEq)]
836pub struct WorldChunkQueryRegionData {
838 pub region: WorldChunkQueryRegion,
840 pub chunks: Vec<ChunkData>,
842 pub stats: ChunkLoadStats,
844}
845
846#[derive(Debug, Clone, Copy, PartialEq, Eq)]
847pub struct ChunkBounds {
849 pub dimension: crate::Dimension,
851 pub min_chunk_x: i32,
853 pub min_chunk_z: i32,
855 pub max_chunk_x: i32,
857 pub max_chunk_z: i32,
859 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)]
885pub struct WorldScanOptions {
887 pub threading: WorldThreadingOptions,
889 pub pipeline: WorldPipelineOptions,
891 pub cancel: Option<CancelFlag>,
893 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)]
909pub enum WorldThreadingOptions {
911 #[default]
912 Auto,
914 Fixed(usize),
916 Single,
918}
919
920pub const MAX_WORLD_THREADS: usize = 512;
922
923impl WorldThreadingOptions {
924 #[must_use]
925 pub fn resolve(self, work_items: usize) -> usize {
927 self.resolve_unchecked(work_items)
928 }
929
930 #[must_use]
931 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 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)]
957pub struct CancelFlag(Arc<AtomicBool>);
959
960impl CancelFlag {
961 #[must_use]
962 pub fn new() -> Self {
964 Self::default()
965 }
966
967 pub fn cancel(&self) {
969 self.0.store(true, Ordering::Relaxed);
970 }
971
972 #[must_use]
973 pub fn from_shared(cancelled: Arc<AtomicBool>) -> Self {
975 Self(cancelled)
976 }
977
978 #[must_use]
979 pub fn to_storage_cancel(&self) -> StorageCancelFlag {
981 StorageCancelFlag::from_shared(Arc::clone(&self.0))
982 }
983
984 #[must_use]
985 pub fn is_cancelled(&self) -> bool {
987 self.0.load(Ordering::Relaxed)
988 }
989}
990
991#[derive(Clone)]
992pub 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 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)]
1022pub struct WorldScanProgress {
1024 pub entries_seen: usize,
1026}
1027
1028impl BedrockWorld<Arc<dyn WorldStorage>> {
1029 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 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 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 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 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 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 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 pub fn storage(&self) -> &dyn WorldStorage {
1167 self.storage.storage()
1168 }
1169
1170 #[must_use]
1171 pub fn path(&self) -> &Path {
1173 &self.path
1174 }
1175
1176 #[must_use]
1177 pub const fn format(&self) -> WorldFormat {
1179 self.format
1180 }
1181
1182 pub fn read_level_dat_blocking(&self) -> Result<LevelDatDocument> {
1184 read_level_dat_document(&self.path.join("level.dat"))
1185 }
1186
1187 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 pub fn compact_storage_blocking(&self) -> Result<()> {
1195 self.ensure_writable()?;
1196 self.storage().compact()
1197 }
1198
1199 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn parse_global_data_blocking(&self) -> Result<Vec<ParsedDbEntry>> {
2297 parse_global_storage_entries(self.storage(), WorldParseOptions::summary())
2298 }
2299
2300 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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
3522pub 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 pub fn put_raw_record(&mut self, key: &ChunkKey, value: impl Into<Bytes>) {
3538 self.batch.put(key.encode(), value.into());
3539 }
3540
3541 pub fn delete_raw_record(&mut self, key: &ChunkKey) {
3543 self.batch.delete(key.encode());
3544 }
3545
3546 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 pub fn delete_raw_key(&mut self, key: impl Into<Bytes>) {
3553 self.batch.delete(key.into());
3554 }
3555
3556 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 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 pub fn delete_map_record(&mut self, id: &MapRecordId) {
3587 self.batch.delete(id.storage_key());
3588 }
3589
3590 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 pub fn delete_global_record(&mut self, kind: &GlobalRecordKind) {
3604 self.batch.delete(kind.storage_key());
3605 }
3606
3607 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 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 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}