How BedrockWriter Saves Minecraft Worlds in Bedrock Edition Format: Implementation Deep Dive

BedrockWriter converts the in-memory WorldToModify representation into a standards-compliant Minecraft Bedrock Edition .mcworld archive by orchestrating LevelDB chunk serialization, NBT metadata generation, and Deflate-compressed zip packaging.

The BedrockWriter struct in src/world_editor/bedrock.rs serves as the final export stage in the Arnis terrain generator. It transforms the internal world model into a playable Bedrock world through a 10-stage pipeline that handles everything from block palette deduplication to temporary directory cleanup.

Architecture Overview

BedrockWriter implements a strict pipeline orchestrated by the write_world method. The process moves from directory initialization through LevelDB population to final archive packaging, with each stage handling a specific Bedrock format requirement.

The implementation relies on several specialized crates declared in Cargo.toml:

  • bedrockrs_level and fastnbt for NBT serialization
  • rusty_leveldb for chunk database interface
  • zip for archive creation

Initialization and Directory Preparation

Constructing the Writer

The BedrockWriter::new method (lines 135-151) initializes the export context:

pub fn new(
    output_path: PathBuf,
    world_name: String,
    spawn: Option<(i32, i32)>,
    ground: Option<Arc<Ground>>,
) -> Self

When the output_path ends with .mcworld, the extension is automatically stripped to create a temporary working directory for the LevelDB files and metadata.

Preparing the Workspace

The prepare_output_dir method (lines 185-199) ensures a clean export environment by removing any stale temporary folders or existing archives, then creates a fresh directory structure that will hold the db/ folder, level.dat, and supporting files.

Writing Level Metadata

Level Name and Version Information

The write_level_name method creates levelname.txt (lines 200-210), a plain text file containing the world name that Bedrock Edition displays in the world selection screen.

The write_level_dat method (lines 212-242) generates the binary NBT file containing all world settings. Using the BedrockLevelDat struct, it serializes:

  • Storage version header (4 bytes)
  • World name and generator settings
  • Spawn coordinates (with optional ground elevation adjustment from the Ground reference)
  • Game rules and version numbers

The NBT payload is written with a 4-byte storage-version header followed by the payload length, ensuring full compatibility with Bedrock's level.dat format expectations.

Serializing Chunks to LevelDB

Database Population Pipeline

The write_chunks_to_db method (lines 250-285) opens a RustyDBInterface on the temporary db/ directory and processes the world hierarchy:

  1. Iterates through every region in WorldToModify
  2. Processes every chunk within each region
  3. Handles every sub-chunk (16x16x16 section) within each chunk

For each sub-chunk, it writes:

  • A version marker (ChunkKey::chunk_marker)
  • A minimal Data3D record containing heightmap and biome placeholder data
  • Encoded sub-chunk data under the appropriate sub-chunk key

Progress updates are emitted via emit_gui_progress_update to maintain the GUI's "Saving Bedrock world..." animation.

Sub-Chunk Encoding Algorithm

The encode_subchunk method (lines 673-735) implements Bedrock sub-chunk format version 9:

fn encode_subchunk(subchunk: &ChunkToModify, y_index: i8) -> Vec<u8>

The encoding process:

  1. Writes header byte 9, layer count 1, and the Y-index
  2. Builds a palette via build_palette_and_indices to deduplicate block states
  3. Selects bits-per-block from Bedrock-allowed values (1, 2, 3, 4, 5, 6, 8, or 16) using bedrock_bits_per_block
  4. Packs block indices into 32-bit words matching Bedrock's Chunker algorithm
  5. Writes palette entries as NBT BedrockBlockState structs

Palette Construction and Block Translation

The build_palette_and_indices method (lines 638-690) creates the block palette essential for Bedrock's compressed chunk format:

  • Starts with the mandatory air entry
  • Traverses blocks in XZY order (Bedrock's expected layout)
  • Converts internal blocks to BedrockBlock using to_bedrock_block_with_properties from src/bedrock_block_map.rs
  • Uses StdHashMap to deduplicate identical block+property combinations
  • Generates a compact palette and 4096-element index map

During encoding, the writer calls:

let block = section.get_block_at_index(internal_idx);
let properties = section.properties.get(&internal_idx);
let bedrock_block = to_bedrock_block_with_properties(block, properties);

This ensures that block states like stair orientation or color variants are preserved correctly in the exported world.

Entity and Block Entity Handling

The write_chunk_entities method (lines 290-320) handles persistent entities and block entities (chests, signs, etc.):

  1. Re-opens the LevelDB using rusty_leveldb::DB for direct access
  2. Writes compound-list NBT records for block entities and entities
  3. Deduplicates entries via dedup_compound_list to avoid duplicate entity IDs

This ensures that any generated structures containing chests or other tile entities remain functional when imported into Bedrock Edition.

Packaging the .mcworld Archive

Archive Structure

The package_mcworld method (lines 727-754) creates the final .mcworld zip archive containing:

  • levelname.txt - World name for the UI
  • metadata.json - Geographic extents and chunk count
  • level.dat - Binary NBT world settings
  • world_icon.jpeg - Built-in thumbnail image
  • db/ - LevelDB directory containing all chunk data

The zip uses Deflate compression method for full compatibility with Minecraft Bedrock Edition's import expectations.

Metadata and Cleanup

The write_metadata method (lines 693-724) creates metadata.json recording the world's geographic bounds and total chunk count, used by the Arnis GUI and external tools for world management.

After successful archive creation, cleanup_temp_dir (lines 758-764) removes the temporary working directory to ensure no orphaned files remain on the host system.

Code Example: Creating a Bedrock World

use arnis::world_editor::bedrock::BedrockWriter;
use arnis::world_editor::common::WorldToModify;
use arnis::coordinate_system::cartesian::XZBBox;
use arnis::coordinate_system::geographic::LLBBox;

// Temporary directory for the world files
let out_dir = std::env::temp_dir().join("my_bedrock_world");

// Empty world representation
let world = WorldToModify::default();
let xz = XZBBox::rect_from_xz_lengths(15.0, 15.0).unwrap();
let ll = LLBBox::new(0.0, 0.0, 1.0, 1.0).unwrap();

BedrockWriter::new(out_dir, "MyWorld".into(), None, None)
    .write_world(&world, &xz, &ll)
    .expect("Failed to write Bedrock world");

This creates MyWorld.mcworld in the temporary directory. To specify a custom spawn point:

let spawn = Some((100, 64)); // X=100, Z=64
BedrockWriter::new(out_dir, "SpawnWorld".into(), spawn, None)
    .write_world(&world, &xz, &ll)
    .unwrap();

The spawn coordinates are injected in write_level_dat (lines 212-224). When ground elevation data is provided, write_level_dat queries it to raise the spawn Y by three blocks (lines 218-229).

Summary

  • BedrockWriter orchestrates a 10-stage pipeline to convert WorldToModify into a .mcworld archive
  • Key stages include LevelDB chunk serialization, NBT metadata generation, and Deflate-compressed zip packaging
  • The implementation uses Bedrock sub-chunk format version 9 with XZY-ordered palette deduplication
  • Block state translation occurs via to_bedrock_block_with_properties in src/bedrock_block_map.rs
  • The writer reports progress to the GUI and cleans up temporary files after archiving

Frequently Asked Questions

What file format does BedrockWriter produce?

BedrockWriter generates a .mcworld file, which is a standard ZIP archive containing a level.dat NBT file, a levelname.txt file, a db/ directory with LevelDB chunk data, and metadata files. This format is directly importable into Minecraft Bedrock Edition on Windows, iOS, Android, and consoles.

How does BedrockWriter handle block property conversion?

The writer delegates block translation to to_bedrock_block_with_properties in src/bedrock_block_map.rs. This function maps internal block identifiers to Bedrock-compatible block names and extracts property maps (such as stair orientation or color variants) from the SectionToModify storage. During palette construction in build_palette_and_indices, these translated blocks are deduplicated while preserving their NBT property states.

What is the sub-chunk format version used by BedrockWriter?

The implementation uses Bedrock sub-chunk format version 9, as implemented in the encode_subchunk method. This format includes a header byte (9), layer count, Y-index, a deduplicated block palette built via build_palette_and_indices, and densely packed block indices using Bedrock's specific bits-per-block values (1, 2, 3, 4, 5, 6, 8, or 16 bits). The packing algorithm matches Bedrock's Chunker 32-bit word alignment requirements.

How does BedrockWriter report progress during world generation?

The writer emits progress updates through emit_gui_progress_update calls within write_chunks_to_db and other stages. This allows the Arnis GUI to display a "Saving Bedrock world..." animation with granular progress indicators as chunks are serialized and written to the LevelDB database, providing real-time feedback during large world exports.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →