Converting Files into Minecraft Worlds

wuemeli1 pts0 comments

Converting files into Minecraft Worlds - Part 1 - WuemeliDid you ever want to store the Minecraft movie in Minecraft? No? Too bad, because now you can.

The Idea

I recently had the silly idea to convert a file into a Minecraft world. This led me down a really cool rabbit-hole, learning a ton about 3D geometry, byte layouts and Minecraft.

The core idea is pretty simple: I want to take any file and be able to view it in Minecraft blocks.

Along the way I also managed to convince my code that a tiny text-file is 2.3 exabytes large.

Why

Why not.

Palette Generator

The first thing you need is a lookup table. A byte can hold one of 256 values (0–255), and luckily there are way more than 256 blocks in Minecraft, so I can hand every single byte its own block. Byte 0 becomes stone, byte 1 becomes granite, all the way up to 255.

A bunch of blocks carry state that changes on its own or depends on placement: wheat, carrots and other crops have an age, water and lava have a level. If byte 42 decoded to wheat and that wheat grew a stage, my decoder would read the wrong byte and the whole file would be in shambles.

I first get the block data for 1.21.11 with mcdata_rs, throw out anything with an age or level state (plus beds and chests, more on those later), take the first 256 blocks, and write them into a Rust file as a fixed array.

( At the end of the video you can see the filter process where the blocks get filtered down to only good blocks )

The code:

use mcdata_rs::mc_data;<br>use std::{fs, path::Path};

const NAUGHTY_LIST: &[&str] = &[<br>"sand",<br>"gravel",<br>"anvil",<br>"dragon_egg",<br>"scaffolding",<br>"dripstone",<br>"snow",<br>"farmland",<br>"chest",<br>"_bed",<br>"door",<br>"ice",<br>"grass_block",<br>];

fn main() {<br>let data_1_21_11 = mc_data("1.21.11").expect("Failed to download Minecraft Block Data");<br>let palette = data_1_21_11<br>.blocks_array<br>.iter()<br>.filter(|b| b.bounding_box.eq("block"))<br>.filter(|b| {<br>!b.states<br>.iter()<br>.any(|s| matches!(s.name.as_str(), "age" | "level" | "part"))<br>})<br>.filter(|b| !NAUGHTY_LIST.iter().any(|g| b.name.contains(g)))<br>.collect::Vec>();

let names: Vec&str> = palette.iter().take(256).map(|b| b.name.as_str()).collect();

let mut out = String::new();<br>out.push_str("pub const PALETTE: [&str; 256] = [\n");<br>//TODO: make this cleaner<br>for name in &names {<br>out.push_str(" \"minecraft:");<br>out.push_str(name);<br>out.push_str("\",\n");<br>out.push_str("];\n");

let out_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/palette.rs");<br>fs::write(out_path, out).expect("Failed to write palette");<br>println!("Wrote {:?} blocks to palette.rs", names.len());<br>That gives me a PALETTE: [&str; 256], and the two functions that do the translation are pretty straightforward:

pub fn byte_to_block(byte: u8) -> &'static str {<br>PALETTE[byte as usize]<br>pub fn block_to_byte(block: &str) -> Resultu8, SulfurError> {<br>PALETTE<br>.iter()<br>.position(|palette_block| *palette_block == block)<br>.map(|i| {<br>u8::try_from(i).expect("palette has exactly 256 entries so this will never happen")<br>})<br>.ok_or(SulfurError::BlockNotInPalette(block.to_string()))<br>byte_to_block is just an array index, and block_to_byte is the reverse lookup.

Doing it

Now that a byte maps to a block, I need to decide where each block goes.

We fill a 16x16 floor first, then move one block up, and once a full 16x16x16 cube is filled, jump to the next one. That 16x16x16 cube is a section, which is 4096 blocks.

One section is only the start though. A Minecraft world goes from Y -64 to Y 320, which is 384 blocks, or 24 sections stacked on top of each other. So instead of stopping after one section, I fill an entire chunk column bottom to top, all 24 sections, before moving sideways to the next chunk.

A region file (.mca) is a 32x32 grid of chunks, so once you multiply it all out:

32 x 32 chunks x 24 sections x 4096 blocks = ~96 MiB per region.

pub fn cube_coords(byte_location: usize) -> silverfish::Coords {<br>const SECTION_SIZE: usize = 16;<br>const BLOCKS_PER_SECTION: usize = SECTION_SIZE * SECTION_SIZE * SECTION_SIZE;<br>const MAX_CHUNKS: usize = 32;<br>const SECTIONS_PER_COLUMN: usize = 24;<br>const MIN_Y: i32 = -64;

let inside_section = byte_location % BLOCKS_PER_SECTION;<br>let section_number = byte_location / BLOCKS_PER_SECTION;

let x_inside = inside_section % SECTION_SIZE;<br>let y_inside = inside_section / (SECTION_SIZE * SECTION_SIZE);<br>let z_inside = (inside_section / SECTION_SIZE) % SECTION_SIZE;

let layer = section_number / SECTIONS_PER_COLUMN;<br>let section_x = layer % MAX_CHUNKS;<br>let section_y = section_number % SECTIONS_PER_COLUMN;<br>let section_z = (layer / MAX_CHUNKS) % MAX_CHUNKS;

let x = u32::try_from(section_x * SECTION_SIZE + x_inside).expect("coordinate overflow");<br>let y =<br>MIN_Y + i32::try_from(section_y * SECTION_SIZE + y_inside).expect("coordinate overflow");<br>let z = u32::try_from(section_z * SECTION_SIZE + z_inside).expect("coordinate overflow");

(x, y, z).into()<br>Maybe the tests make it easier to follow:

byte 0 → (0, -64, 0) - very bottom of the world

byte 1 → (1, -64, 0)

byte 15 → (15,...

byte section_size minecraft palette blocks block

Related Articles