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

> Explore BedrockWriter implementation details for saving Minecraft worlds. Learn how it converts WorldToModify to .mcworld archives using LevelDB serialization, NBT metadata, and Deflate compression.

- Repository: [Louis Erbkamm/arnis](https://github.com/louis-e/arnis)
- Tags: deep-dive
- Published: 2026-03-20

---

**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`](https://github.com/louis-e/arnis/blob/main/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`](https://github.com/louis-e/arnis/blob/main/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:

```rust
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`](https://github.com/louis-e/arnis/blob/main/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:

```rust
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`](https://github.com/louis-e/arnis/blob/main/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:

```rust
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`](https://github.com/louis-e/arnis/blob/main/levelname.txt) - World name for the UI
- [`metadata.json`](https://github.com/louis-e/arnis/blob/main/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`](https://github.com/louis-e/arnis/blob/main/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

```rust
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:

```rust
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`](https://github.com/louis-e/arnis/blob/main/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`](https://github.com/louis-e/arnis/blob/main/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`](https://github.com/louis-e/arnis/blob/main/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.