2026年新尝试:将文件转换为Minecraft世界,还能存《Minecraft》电影!
将文件转换为Minecraft世界 - 第1部分2026年7月20日有人提出一个新奇问题是否曾想过把《Minecraft》电影存进Minecraft里若答案是没有那现在有了新可能因为可以做到了。1. 想法来源此人最近冒出把文件转换成Minecraft世界的想法这让其踏上探索之旅期间学到大量关于3D几何、字节布局和Minecraft的知识。核心想法很简单就是把任何文件以Minecraft方块形式展示过程中还让代码误以为一个小小文本文件有2.3艾字节那么大。2. 为什么要这么做原因是不试试怎么知道不行呢。3. 调色板生成器首先需要一个查找表。一个字节可表示256个值0 - 255而Minecraft里方块远不止256种所以能给每个字节分配独特方块如字节 0 对应石头字节 1 对应花岗岩以此类推到 255。不过有些方块有自己状态会自行改变或取决于放置方式像小麦、胡萝卜等农作物有“生长阶段age”水和岩浆有“流动等级level”。若字节 42 解码成小麦小麦生长一个阶段解码器就会读取错误字节整个文件会乱套。此人先用 mcdata_rs 获取1.21.11版本的方块数据剔除所有有“生长阶段”或“流动等级”状态的方块还有床和箱子后面会详细说选取前256个方块然后把它们作为固定数组写入Rust文件。在视频结尾能看到过滤过程方块会被筛选出符合要求的代码如下rustuse mcdata_rs::mc_data;use std::{fs, path::Path};const NAUGHTY_LIST: [str] [sand,gravel,anvil,dragon_egg,scaffolding,dripstone,snow,farmland,chest,_bed,door,ice,grass_block,];fn main() {let data_1_21_11 mc_data(1.21.11).expect(Failed to download Minecraft Block Data);let palette data_1_21_11.blocks_array.iter().filter(|b| b.bounding_box.eq(block)).filter(|b| {!b.states.iter().any(|s| matches!(s.name.as_str(), age | level | part))}).filter(|b| !NAUGHTY_LIST.iter().any(|g| b.name.contains(g))).collect::();let names: Vecstr palette.iter().take(256).map(|b| b.name.as_str()).collect();let mut out String::new();out.push_str(pub const PALETTE: [str; 256] [\n);//TODO: make this cleanerfor name in names {out.push_str( \minecraft:);out.push_str(name);out.push_str(\,\n);}out.push_str(];\n);let out_path Path::new(env!(CARGO_MANIFEST_DIR)).join(src/palette.rs);fs::write(out_path, out).expect(Failed to write palette);println!(Wrote {:?} blocks to palette.rs, names.len());}这样得到一个 PALETTE: [str; 256]实现转换的两个函数也很简单rustpub fn byte_to_block(byte: u8) - static str {PALETTE[byte as usize]}pub fn block_to_byte(block: str) - Result {PALETTE.iter().position(|palette_block| *palette_block block).map(|i| {u8::try_from(i).expect(palette has exactly 256 entries so this will never happen)}).ok_or(SulfurError::BlockNotInPalette(block.to_string()))}byte_to_block 是简单的数组索引block_to_byte 则是反向查找。4. 具体操作现在每个字节都对应一个方块接下来要决定每个方块的放置位置。先填满一个16x16的平面然后向上移动一层当一个16x16x16的立方体填满后再跳到下一个立方体。这个16x16x16的立方体就是一个“区段section”包含4096个方块。不过一个区段只是开始。Minecraft世界的Y轴范围从 -64到320总共384个方块也就是24个区段堆叠在一起。所以不是填满一个区段就停止而是从下到上填满一整个“区块列chunk column”的24个区段然后再移动到旁边的下一个区块。一个区域文件.mca是一个32x32的区块网格全部算下来32 x 32个区块 x 24个区段 x 4096个方块 每个区域约96 MiBrustpub fn cube_coords(byte_location: usize) - silverfish::Coords {const SECTION_SIZE: usize 16;const BLOCKS_PER_SECTION: usize SECTION_SIZE * SECTION_SIZE * SECTION_SIZE;const MAX_CHUNKS: usize 32;const SECTIONS_PER_COLUMN: usize 24;const MIN_Y: i32 -64;let inside_section byte_location % BLOCKS_PER_SECTION;let section_number byte_location / BLOCKS_PER_SECTION;let x_inside inside_section % SECTION_SIZE;let y_inside inside_section / (SECTION_SIZE * SECTION_SIZE);let z_inside (inside_section / SECTION_SIZE) % SECTION_SIZE;let layer section_number / SECTIONS_PER_COLUMN;let section_x layer % MAX_CHUNKS;let section_y section_number % SECTIONS_PER_COLUMN;let section_z (layer / MAX_CHUNKS) % MAX_CHUNKS;let x u32::try_from(section_x * SECTION_SIZE x_inside).expect(coordinate overflow);let y MIN_Y i32::try_from(section_y * SECTION_SIZE y_inside).expect(coordinate overflow);let z u32::try_from(section_z * SECTION_SIZE z_inside).expect(coordinate overflow);(x, y, z).into()}也许通过测试用例能更容易理解字节 0 → (0, -64, 0) - 世界的最底部字节 1 → (1, -64, 0)字节 15 → (15, -64, 0) - 第一行的末尾字节 16 → (0, -64, 1) - 下一行字节 255 → (15, -64, 15) - 平面填满字节 256 → (0, -63, 0) - 向上一层字节 4095 → (15, -49, 15) - 区段填满字节 4096 → (0, -48, 0) - 上一个区段仍在同一区块列内字节 4096 * 24 → (16, -64, 0) - 整个区块列填满24个区段移动到下一个区块字节 4096 * 24 * 32 → (0, -64, 16) - 这一行区块填满Z轴换行最终结果如下编码后的数据具体是我的Cargo.lock文件编码读取文件创建一个空的区域写入一个小的头部信息下面会详细介绍然后遍历每个字节将其转换为方块并放置在对应的 cube_coords 位置最后将其写入 .mca 区域文件。rustpub fn file_to_region(source_file: impl AsRef,region_file: impl AsRef,) - Result(), SulfurError {let source_file source_file.as_ref();let region_file region_file.as_ref();if !source_file.exists() {return Err(SulfurError::InputFileNotFound);}let input_file std::fs::read(source_file)?;if HEADER_SIZE input_file.len() REGION_CAPACITY {return Err(SulfurError::EncodedPayloadTooLarge);}let mut region Region::default();region.set_config(Config::new(true, true, Config::DEFAULT_WORLD_HEIGHT))?;let header Header::new(input_file.len() as u64).to_bytes();for (location, byte) in header.iter().enumerate() {region.set_block(cube_coords(location), byte_to_block(*byte))?;}for (byte_location, byte) in input_file.iter().enumerate() {region.set_block(cube_coords(header.len() byte_location),byte_to_block(*byte),)?;}region.write_blocks()?;region.write(mut std::fs::File::create(region_file)?)?;Ok(())}解码加载区域文件将头部方块读取为字节以确定原始文件的大小然后读取相应数量的有效负载方块将每个方块通过 block_to_byte 转换为字节并将这些字节写回磁盘。rustpub fn region_to_file(region_file: impl AsRef,output_file: impl AsRef,) - Result(), SulfurError {let region_file region_file.as_ref();let output_file output_file.as_ref();if !region_file.exists() {return Err(SulfurError::InputFileNotFound);}let region Region::from_region(mut std::fs::File::open(region_file)?, (0, 0))?;let header_coords: Vec (0..HEADER_SIZE).map(cube_coords).collect();let header_batch region.get_blocks(header_coords)?;let raw_header_bytes header_coords.iter().map(|coord| {let block header_batch.get(*coord)?.ok_or(SulfurError::MissingBlockAt(*coord))?;block_to_byte(block.name.to_str())}).collect::, _()?;let (header, header_len) Header::from_bytes(raw_header_bytes)?;let file_size usize::try_from(header.file_size).map_err(|_| SulfurError::EncodedPayloadTooLarge)?;let end header_len.checked_add(file_size).ok_or(SulfurError::EncodedPayloadTooLarge)?;let coords: Vec (header_len..end).map(cube_coords).collect();let batch region.get_blocks(coords)?;let mut file_buffer std::io::BufWriter::new(std::fs::File::create(output_file)?);for coord in coords {let block batch.get(*coord)?.ok_or(SulfurError::MissingBlockAt(*coord))?;file_buffer.write_all([block_to_byte(block.name.to_str())?])?;}file_buffer.flush()?;Ok(())}5. 我犯过的错误扁平板问题最初版本的 cube_coords 虽能工作但浪费大部分世界空间。只在Y轴为 0 开始的16个方块高度的单层放置方块并且将区段水平展开rustpub fn cube_coords(byte_location: usize) - silverfish::Coords {const SECTION_SIZE: usize 16;const BLOCKS_PER_SECTION: usize SECTION_SIZE * SECTION_SIZE * SECTION_SIZE;const BASE_Y: usize 0;const MAX_BLOCKS: usize 32;let inside_section byte_location % BLOCKS_PER_SECTION;let section_number byte_location / BLOCKS_PER_SECTION;let x_inside inside_section % SECTION_SIZE;let y_inside inside_section / (SECTION_SIZE * SECTION_SIZE);let z_inside (inside_section / SECTION_SIZE) % SECTION_SIZE;let section_x section_number % MAX_BLOCKS;let section_z (section_number / MAX_BLOCKS) % MAX_BLOCKS;let x u32::try_from(section_x * SECTION_SIZE x_inside).expect(coordinate overflow);let y i32::try_from(BASE_Y y_inside).expect(coordinate overflow);let z u32::try_from(section_z * SECTION_SIZE z_inside).expect(coordinate overflow);(x, y, z).into()}由于 y 始终不超过15每个世界都变成一个512x512的扁平板只有16个方块高。而一个区域原本可容纳约96 MiB的数据现在只能容纳约4 MiB。容量溢出问题还记得那个2.3艾字节吗问题出在试图读取 .mca 文件的前8个字节并把它当作文件大小。这得到一个值 2338328528344327162然后还天真地把它当作文件长度……也就是2.3艾字节。实际上原始的区域文件有自己的格式和头部信息读取的前几个字节根本不是数据。解决办法是使用自定义头部rustconst MAGIC: [u8; 4] *bSULF;// magic sizepub const HEADER_SIZE: usize 12;pub struct Header {pub file_size: u64,}impl Header {pub fn new(file_size: u64) - Self {Self { file_size }}pub fn to_bytes(self) - Vec {let mut bytes Vec::new();bytes.extend(MAGIC);bytes.extend(self.file_size.to_le_bytes());bytes}pub fn from_bytes(bytes: [u8]) - Result(Self, usize), SulfurError {if bytes.len() HEADER_SIZE {return Err(SulfurError::InvalidHeaderSize(HEADER_SIZE, bytes.len()));}if bytes[0..4] ! MAGIC {return Err(SulfurError::InvalidMagicBytes);}let size_bytes bytes[MAGIC.len()..HEADER_SIZE].try_into().map_err(|_| SulfurError::InvalidHeaderSize(HEADER_SIZE, bytes.len()))?;let file_size u64::from_le_bytes(size_bytes);Ok((Self::new(file_size), HEADER_SIZE))}}隐形方块问题床和双箱子会显示为隐形。它们都是“多方块结构”床有床头和床尾双箱子由两个半箱组成。虽然这不会影响解码器的工作但看起来实在太丑了。只好把它们添加到 NAUGHTY_LIST 数组中。6. 后续计划Jeb真的很厉害他编写了Anvil文件格式。没错《Minecraft》电影也能存进去不过要大幅降采样。未来打算添加一些苦力怕/TNT防护机制让程序能够输出大型多区域世界这样无论文件有多大都能进行编码。此人之前从没写过这类博客文章若有改进建议可联系。若想亲自尝试可以访问相关仓库。关于大语言模型的观点总结Listen up, heres the truth about LLMs...LLMs bring risks, privacys at stake, Datas exposed, its a hard mistake. Jobs might be lost, displacements near, Bias in the code, its crystal clear. Oversight is key, we must take heed, To make sure these models dont mislead. Lets use them right, with caution and care, For a future where tech is always fair.