bedrock-world 0.2.1

Async Minecraft Bedrock world, level.dat, NBT, player and chunk library
Documentation

bedrock-world

English | 简体中文

bedrock-world is a Minecraft Bedrock world library built on top of bedrock-leveldb. It provides fast level.dat access, little-endian NBT, Bedrock DB key classification, player reads, chunk/subchunk parsing including LevelDB-era legacy terrain records, entity and block-entity parsing, item extraction, biome summaries, and typed map/village/global record access.

The performance model is benchmark-backed rather than "femtosecond" marketing: hot paths avoid owned raw-value retention, use borrowed/event NBT views when a DOM is not needed, and keep shared cache locks opt-in.

This crate focuses on complete parsing behavior. The bedrock-dev/bedrock-level project is referenced only for parsing behavior.

Recommended API

  • read_level_dat(path) and write_level_dat_atomic(path, document) are the launcher fast path. They do not open LevelDB.
  • BedrockWorld::open(path, OpenOptions) creates a lazy world handle backed by bedrock-leveldb or the read-only legacy chunks.dat backend; it does not parse the full world.
  • BedrockWorld<S> is generic over the storage handle. Compatibility constructors such as open_blocking, open, and from_storage return the dynamic Arc<dyn WorldStorage> form. Hot paths can use BedrockWorld::from_typed_storage or BedrockWorld::open_typed_blocking to keep BedrockLevelDbStorage or MemoryStorage as the concrete backend.
  • OpenOptions::format defaults to WorldFormatHint::Auto. Auto opens db/CURRENT worlds as LevelDB, marks early StorageVersion <= 4 worlds as WorldFormat::LevelDbLegacyTerrain, and opens pre-LevelDB chunks.dat worlds as WorldFormat::PocketChunksDat.
  • OpenOptions::default() is read-only. Any world-record write must reopen the world with OpenOptions { read_only: false, ..OpenOptions::default() }. Read-only worlds return BedrockWorldErrorKind::ReadOnly from high-level writes before touching storage.
  • Use category APIs for UI and tools: classify_keys_blocking, list_players_blocking, list_chunk_positions_blocking, parse_chunk_blocking, parse_subchunk_blocking, scan_entities_blocking, scan_block_entities_blocking, scan_items_blocking, scan_maps_blocking, scan_villages_blocking, and scan_globals_blocking.
  • Use typed v0.2 BedrockLevelFormat write APIs on a writable world: write_map_record_blocking, delete_map_record_blocking, write_global_record_blocking, delete_global_record_blocking, put_heightmap_blocking, put_biome_storage_blocking, put_hsa_for_chunk_blocking, delete_hsa_for_chunk_blocking, put_block_entities_blocking, edit_block_entity_at_blocking, delete_block_entity_at_blocking, put_actor_blocking, delete_actor_blocking, and move_actor_blocking. Matching async wrappers are available behind the default async feature.
  • High-level writes serialize and parse records back before commit. Actor writes update digp -> actorprefix records in one transaction. Block-entity writes validate coordinates against the target chunk. PocketChunksDatStorage remains read-only.
  • bedrock-world stops at Bedrock key/value semantics. Post-write refresh, invalidation, and presentation policy belong to downstream applications or adapter crates.
  • Async wrappers use tokio::task::spawn_blocking, so disk and decode work does not block the foreground async runtime.
  • WorldScanOptions controls threading, cancellation, and progress callbacks.
  • NbtReader::view().events() provides a borrowed event stream for tools that need to inspect NBT without constructing an owned NbtTag DOM.
  • WorldPipelineOptions refines the bounded pipeline with queue depth, chunk batch size, subchunk decode worker budget, and progress cadence. Zero values choose automatic defaults.
  • Render-specific APIs now have their own fast path: list_render_chunk_positions_blocking, list_render_chunk_positions_in_region_blocking, load_render_chunk_blocking, load_render_chunks_blocking, and load_render_region_blocking. These only read records needed to render chunks and can run with bounded parallelism.
  • Render chunk data now carries legacy_terrain: Option<LegacyTerrain>, structured legacy_biomes, and compatibility legacy_biome_colors. LegacyTerrain biome samples are decoded as [biome_id, red, green, blue]; the compatibility color is exposed as 0x00RRGGBB. Exact surface sampling prefers those saved legacy RGB samples over conflicting old Data2D/Data3D biome ids and records legacy_biome_preferred_columns in load stats. LegacyTerrain records are requested through exact batch reads, so 0.16-era LevelDB worlds do not need Data2D or SubChunkPrefix to be considered renderable.
  • RenderChunkLoadOptions::request selects one render load contract: ExactSurface computes canonical top-down surface columns, RawHeightMap loads only raw height records for diagnostics, and Layer/Biome load fixed slices. ExactSurface exposes RenderChunkData::column_samples with the real visual surface block, relief/support block, optional thin overlay, water context, biome sample, and source for every sampled X/Z column.
  • Transition chunks that contain both LegacyTerrain and SubChunkPrefix keep both records; renderers should prefer subchunk block data and use legacy terrain/biome colors only as fallbacks.
  • parse_world_blocking(WorldParseOptions) is an explicit advanced/offline API, not a launcher default path.
  • Public fallible APIs return bedrock_world::Result<T>. Match BedrockWorldError::kind() for stable categories such as read-only handles, cancellation, malformed NBT, unsupported chunk formats, and backend errors.

More detailed API, testing, and benchmark notes are in docs/API.md, docs/TESTING.md, and docs/BENCHMARKS.md.

use bedrock_world::{
    read_level_dat, BedrockWorld, OpenOptions, WorldScanOptions, WorldThreadingOptions,
};

async fn inspect_world() -> bedrock_world::Result<()> {
    let level = read_level_dat("path/to/minecraftWorld/level.dat")?;
    println!("level.dat version={}", level.header.version);

    let world = BedrockWorld::open("path/to/minecraftWorld", OpenOptions::default()).await?;
    let players = world.list_players().await?;
    println!("players={}", players.len());

    let key_counts = world
        .classify_keys(WorldScanOptions {
            threading: WorldThreadingOptions::Auto,
            ..WorldScanOptions::default()
        })
        .await?;
    println!("key categories={}", key_counts.len());

    Ok(())
}

Parse Strategies

Strategy Raw entries Raw values Subchunk indices Actor resolution Intended use
WorldParseOptions::summary() no no counts only referenced actors UI summaries, large scans
WorldParseOptions::structured() selected parsed entries no counts only referenced actors inspection tools
WorldParseOptions::full_raw() / full() yes yes full 4096 indices all actors debugging/offline analysis

WorldParseCategories controls whether chunks, players, entities, block entities, items, maps, villages, globals, and key counts are parsed. Summary mode keeps counts and structured summaries while avoiding raw value retention.

Performance Model

  • Launcher operations that only need level.dat should use read_level_dat and write_level_dat_atomic; this path touches only level.dat.
  • BedrockWorld::open is lazy and delegates DB access to bedrock-leveldb.
  • Key classification uses key-only scans and does not retain values.
  • Viewport rendering should use list_render_chunk_positions_in_region_blocking or its async wrapper before loading render chunks. This probes each visible chunk with key-only prefix scans and skips chunks that have no render records.
  • For interactive tile rendering, load_render_chunks_with_stats_blocking uses exact get_many requests for LegacyTerrain, biome records, subchunks, and block entities. RenderLoadStats::prefix_scans should remain 0 on this exact path; legacy_terrain_records, legacy_biome_samples, legacy_biome_colors, terrain_source_legacy, terrain_source_subchunk, legacy_pocket_chunks, and detected_format identify old-world and transition-world loads.
  • Exact render chunk batches preserve the association between every requested ChunkPos and its records even when the input is shuffled, duplicated, or resorted by RenderChunkPriority. If a renderer shows chunk-level visual scrambling, compare these exact-batch stats with renderer placement diagnostics before changing parser coordinate formulas.
  • Chunk parsing uses prefix scans and the LevelDB native block cache; repeated sample chunk reads avoid full table scans.
  • Default world scans use automatic bounded parallel table scanning. Use WorldThreadingOptions::Single for deterministic debugging.
  • RenderChunkLoadOptions::threading and RenderRegionLoadOptions::threading control parallel render chunk loading. Use Single when an outer renderer already owns the worker pool to avoid nested oversubscription.
  • RenderChunkLoadOptions::priority can use RenderChunkPriority::DistanceFrom { chunk_x, chunk_z } so the current viewport center loads first. RenderRegionData::stats reports requested and loaded chunks, decoded subchunks, worker count, queue wait, and total load time.
  • Long scans can be cancelled through CancelFlag and can report progress through ProgressSink.

Current Large Fixture Baseline

Local run on Windows, Rust bench profile, 2026-05-03, fixture C:\Users\Administrator\Desktop\BE-Community-Dev\bedrock-world\tests\fixtures\sample-bedrock-world. The fixture is local-only data and is not a CI contract.

Latest large-fixture numbers are tracked in docs/BENCHMARKS.md.

Legacy World Formats

bedrock-world keeps LevelDB and pre-LevelDB worlds behind the same WorldStorage abstraction:

  • WorldFormat::LevelDb for current Bedrock LevelDB worlds.
  • WorldFormat::LevelDbLegacyTerrain for old LevelDB worlds whose renderable chunk data is stored in LegacyTerrain tag 0x30.
  • WorldFormat::PocketChunksDat for old Pocket Edition worlds with chunks.dat. The PocketChunksDatStorage backend is read-only and exposes each terrain payload as a virtual LegacyTerrain record. Mutating methods return UnsupportedChunkFormat.
let world = bedrock_world::BedrockWorld::open_blocking(
    "path/to/minecraftWorld",
    bedrock_world::OpenOptions::default(),
)?;
println!("detected format: {:?}", world.format());

Migration: full chunk scan to viewport render index

Old map viewers often waited for a full-world chunk scan before rendering:

let all_chunks = world
    .list_chunk_positions_blocking(WorldScanOptions::default())?;
let visible = all_chunks
    .into_iter()
    .filter(|pos| viewport.contains(*pos))
    .collect::<Vec<_>>();

Prefer a render-only region query for the current viewport:

let visible = world.list_render_chunk_positions_in_region_blocking(
    bedrock_world::RenderChunkRegion {
        dimension,
        min_chunk_x,
        min_chunk_z,
        max_chunk_x,
        max_chunk_z,
    },
    WorldScanOptions {
        threading: WorldThreadingOptions::Auto,
        cancel: Some(cancel),
        progress: Some(progress),
        ..WorldScanOptions::default()
    },
)?;

Then load just those chunks for the tile or viewport batch:

let chunks = world.load_render_chunks_blocking(
    visible,
    bedrock_world::RenderChunkLoadOptions {
        threading: WorldThreadingOptions::Fixed(4),
        priority: bedrock_world::RenderChunkPriority::DistanceFrom {
            chunk_x: viewport_center_x,
            chunk_z: viewport_center_z,
        },
        ..bedrock_world::RenderChunkLoadOptions::default()
    },
)?;

Fixture Result

The optional fixture at tests/fixtures/sample-bedrock-world is a large local Bedrock world with native .ldb tables and WAL data. It is intentionally ignored by Git because real worlds are large and may contain player data. When the folder is missing, the fixture test and large-world benches print a skip message and continue successfully.

cargo test -p bedrock-world -- --nocapture

unit tests: 36 passed; 0 failed; finished in 0.03s
fixture test: 1 passed; 0 failed; finished in 15.85s

db.entries.count=4571643
db.entries.key_bytes=63624175
db.entries.value_bytes=8398184492
db.chunk.positions.count=237534
db.unknown_keys.first=[]
parsed.sample_chunk.pos=ChunkPos { x: 451, z: -457, dimension: End }
parsed.sample_chunk.records=10
parsed.sample_chunk.subchunks=5
parsed.sample_chunk.subchunk_storages=4
parsed.sample_chunk.palette_states=10
parsed.sample_chunk.block_entities=0
parsed.sample_chunk.biomes.records=1 storages=25
parsed.sample_chunk.errors=[]
players.count=290

Benchmark Results

Latest local Criterion and large-fixture results are tracked in docs/BENCHMARKS.md. The one-shot large fixture harness is intentionally separate from Criterion because multi-million-entry scans should not be repeated inside microbenchmarks.

Features And docs.rs

docs.rs builds with all features enabled, so the hosted API reference includes async wrappers and the optional bedrock-leveldb backend.

Feature Default Meaning
async yes Adds async wrappers that delegate blocking filesystem, LevelDB, and NBT work to tokio::task::spawn_blocking
backend-bedrock-leveldb yes Enables opening native Bedrock LevelDB worlds through bedrock-leveldb
leveldb-mmap no Enables the backend and forwards the bedrock-leveldb/mmap feature

Disable default features when a tool only needs pure parsing, in-memory storage, level.dat, or NBT helpers. The crates.io package includes the English and Chinese READMEs, guide documents under docs/, the changelog, licenses, source, tests, fixture documentation, and benchmarks.

Completeness

Area Status
level.dat header, warning, atomic write Implemented
Bedrock little-endian NBT and consecutive roots Implemented
DB key classification Implemented for chunk, player, actorprefix, digp, map, village, local player aliases, and common global keys
Legacy LegacyTerrain records Implemented for 83,200-byte LevelDB-era terrain values
Legacy subchunk block arrays Implemented for v0 and v2-v7 pre-paletted SubChunkPrefix values
Subchunk v1/v8/v9 palette parsing Implemented, counts-only and full-indices modes
Data2D/Data3D biome and heightmap codecs Implemented
HSA, map, global, actor, and block-entity writes Implemented with roundtrip validation
digp -> actorprefix actor resolution Implemented, configurable, with transactional modern actor writes
Players, entities, block entities, item stacks Implemented common field extraction
Unknown version-specific data Preserved or counted according to retention mode
Full structured editing for every chunk version Not implemented
Map pixel record parsing Implemented

Historical Bedrock worlds that predate LevelDB and store chunk data in chunks.dat / entities.dat are not database worlds; importing those files is outside this crate's current scope.

Operational Guidance

  • Do not call parse_world_blocking(WorldParseOptions::full_raw()) from UI code. It is an offline debugging path.
  • Use read_level_dat for launcher metadata and write_level_dat_atomic for safe level.dat edits.
  • Use category APIs when only one class of data is required. This avoids parsing entities, chunks, and global records unnecessarily.
  • For renderers, build a viewport RenderChunkRegion first and use the render-index APIs. Keep full list_chunk_positions_blocking for metadata, search, and offline export workflows.
  • The optional bedrock-leveldb backend uses a versioned dependency for crates.io publishing and a local ../bedrock-leveldb path for repository development.