From aec030793939cad709221fbf3021c4914d733367 Mon Sep 17 00:00:00 2001
From: neoapps-dev
Date: Wed, 15 Jul 2026 23:34:49 +0300
Subject: [PATCH 1/4] feat: xbox360+ps3->windows64 world conversion
---
README.md | 1 +
src-tauri/src/commands/console2lce.rs | 1193 +++++++++++++++++++-
src-tauri/src/lib.rs | 1 +
src/components/modals/ImportWorldModal.tsx | 159 ++-
src/services/TauriService.ts | 7 +
5 files changed, 1319 insertions(+), 42 deletions(-)
diff --git a/README.md b/README.md
index 6f2d1ba..876c90b 100644
--- a/README.md
+++ b/README.md
@@ -184,6 +184,7 @@ sudo apt install --reinstall libwebkit2gtk-4.1-0
- **4J Studios & Mojang** - Original creators of Legacy Console Edition
- **The LCE Community** - Research and foundations for LCE on PC
- **Str1k3r** - Original creator of LCEOnline
+- **DTention** - Original creator of [LCE-Save-Converter](https://github.com/dtentiion/LCE-Save-Converter)
---
diff --git a/src-tauri/src/commands/console2lce.rs b/src-tauri/src/commands/console2lce.rs
index 503749e..fd5a75f 100644
--- a/src-tauri/src/commands/console2lce.rs
+++ b/src-tauri/src/commands/console2lce.rs
@@ -1,6 +1,1157 @@
-use std::path::Path;
+use std::collections::HashMap;
+use std::collections::HashSet;
use std::fs;
+use std::io::{Read, Write};
+use std::path::Path;
use tauri::AppHandle;
+const STFS_MAGIC: [u8; 4] = *b"CON ";
+const STFS_BASE_OFFSET: usize = 0xA000;
+const STFS_BLOCK_SIZE: usize = 0x1000;
+const STFS_BLOCKS_PER_GRP: usize = 170;
+const FILE_ENTRY_SIZE: usize = 144;
+const REGION_SECT_COUNT: usize = 1024;
+const LZX_BLOCK_SIZE: usize = 0x8000;
+const SFO_FMT_UTF8_RAW: u16 = 0x0004;
+const SFO_FMT_UTF8_STR: u16 = 0x0204;
+const SFO_FMT_INT32: u16 = 0x0404;
+const LEVELNAME_SIG: &[u8] = b"\x08\x00\x09LevelName";
+const PLAYER_POS_SIG: &[u8] = b"\x09\x00\x03Pos\x06\x00\x00\x00\x03";
+struct StfsEntry {
+ name: String,
+ start_block: usize,
+ size: usize,
+}
+
+struct FileEntry {
+ filename: String,
+ length: u32,
+ start_offset: u32,
+ last_mod: i64,
+}
+
+fn s32(buf: &mut [u8], o: usize) {
+ buf[o..o + 4].reverse();
+}
+
+fn read_hdr_be(data: &[u8]) -> (u32, u32, i16, i16) {
+ let ho = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
+ let ne = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
+ let ov = i16::from_be_bytes([data[8], data[9]]);
+ let cv = i16::from_be_bytes([data[10], data[11]]);
+ (ho, ne, ov, cv)
+}
+
+fn parse_ftable_be(data: &[u8], ho: u32, ne: u32) -> Vec {
+ let mut out = Vec::new();
+ for i in 0..ne {
+ let base = ho as usize + i as usize * FILE_ENTRY_SIZE;
+ if base + FILE_ENTRY_SIZE > data.len() {
+ break;
+ }
+ let raw = &data[base..base + FILE_ENTRY_SIZE];
+ let fn_u16s: Vec = (0..64)
+ .map(|j| u16::from_be_bytes([raw[j * 2], raw[j * 2 + 1]]))
+ .collect();
+ let fn_str: String = fn_u16s
+ .iter()
+ .take_while(|&&c| c != 0)
+ .filter_map(|&c| char::from_u32(c as u32))
+ .collect();
+ let length = u32::from_be_bytes([raw[128], raw[129], raw[130], raw[131]]);
+ let start_offset = u32::from_be_bytes([raw[132], raw[133], raw[134], raw[135]]);
+ let last_mod = i64::from_be_bytes([
+ raw[136], raw[137], raw[138], raw[139], raw[140], raw[141], raw[142], raw[143],
+ ]);
+ if !fn_str.is_empty() && length > 0 {
+ out.push(FileEntry {
+ filename: fn_str,
+ length,
+ start_offset,
+ last_mod,
+ });
+ }
+ }
+ out
+}
+
+fn sanitise(name: &str) -> String {
+ let mut s = String::new();
+ let mut prev_underscore = false;
+ for c in name.chars() {
+ let replace = c.is_control()
+ || matches!(
+ c,
+ '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*'
+ );
+ let c = if replace { '_' } else { c };
+ if c == '_' {
+ if !prev_underscore {
+ s.push(c);
+ }
+ prev_underscore = true;
+ } else {
+ s.push(c);
+ prev_underscore = false;
+ }
+ }
+ let s = s.trim_matches(|c: char| c == '_' || c == '.' || c == ' ');
+ let s = if s.len() > 64 { &s[..64] } else { s };
+ if s.is_empty() {
+ "MinecraftSave".to_string()
+ } else {
+ s.to_string()
+ }
+}
+
+fn stfs_block_offset(block_num: usize, table_shift: usize) -> usize {
+ let g = block_num / STFS_BLOCKS_PER_GRP;
+ let hash_before = if g == 0 {
+ 2
+ } else {
+ 3 + 2 * g + table_shift
+ };
+ STFS_BASE_OFFSET + (block_num + hash_before) * STFS_BLOCK_SIZE
+}
+
+fn stfs_get_hash_entry(raw: &[u8], block_num: usize, table_shift: usize) -> Option {
+ let group = block_num / STFS_BLOCKS_PER_GRP;
+ let first_data = stfs_block_offset(group * STFS_BLOCKS_PER_GRP, table_shift);
+ let idx = (block_num % STFS_BLOCKS_PER_GRP) * 0x18;
+ for gap in &[2, 1] {
+ let hash_off = first_data - STFS_BLOCK_SIZE * gap;
+ let entry_off = hash_off + idx;
+ if entry_off + 0x18 > raw.len() {
+ continue;
+ }
+ let nxt = ((raw[entry_off + 0x15] as usize) << 16)
+ | ((raw[entry_off + 0x16] as usize) << 8)
+ | raw[entry_off + 0x17] as usize;
+ if nxt != 0xFFFFFF {
+ return Some(nxt);
+ }
+ }
+ None
+}
+
+fn stfs_read_file(raw: &[u8], start: usize, size: usize, table_shift: usize) -> Vec {
+ let max_block = (raw.len().saturating_sub(STFS_BASE_OFFSET)) / STFS_BLOCK_SIZE;
+ let mut out = Vec::with_capacity(size);
+ let mut blk = start;
+ let mut rem = size;
+ while rem > 0 {
+ let off = stfs_block_offset(blk, table_shift);
+ let end = (off + STFS_BLOCK_SIZE).min(raw.len());
+ if off >= raw.len() {
+ break;
+ }
+ let chunk = &raw[off..end];
+ let take = rem.min(chunk.len());
+ out.extend_from_slice(&chunk[..take]);
+ rem -= take;
+ match stfs_get_hash_entry(raw, blk, table_shift) {
+ Some(nxt) if nxt > 0 && nxt < max_block => blk = nxt,
+ _ => blk += 1,
+ }
+ }
+ out
+}
+
+struct StfsPackage {
+ raw: Vec,
+ table_shift: usize,
+}
+
+impl StfsPackage {
+ const DISP_OFF: usize = 0x0411;
+ const DISP_LEN: usize = 128;
+ const THUMB_OFF: usize = 0x171A;
+ fn new(raw: Vec) -> Result {
+ if raw.len() < 4 || raw[..4] != STFS_MAGIC {
+ return Err(format!(
+ "Not an STFS CON file (magic={:?})",
+ &raw[..4.min(raw.len())]
+ ));
+ }
+ Ok(Self {
+ raw,
+ table_shift: 1,
+ })
+ }
+
+ fn read_block(&self, b: usize) -> Vec {
+ let o = stfs_block_offset(b, self.table_shift);
+ let end = (o + STFS_BLOCK_SIZE).min(self.raw.len());
+ self.raw[o..end].to_vec()
+ }
+
+ fn display_name(&self) -> String {
+ let start = Self::DISP_OFF;
+ let end = (start + Self::DISP_LEN * 2).min(self.raw.len());
+ let nb = &self.raw[start..end];
+ let mut name = String::new();
+ let mut i = 0;
+ while i + 1 < nb.len() {
+ let c = u16::from_be_bytes([nb[i], nb[i + 1]]);
+ if c == 0 {
+ break;
+ }
+ if let Some(ch) = char::from_u32(c as u32) {
+ name.push(ch);
+ }
+ i += 2;
+ }
+ if name.is_empty() {
+ "Unknown".to_string()
+ } else {
+ name
+ }
+ }
+
+ fn thumbnail(&self) -> Option> {
+ let png_magic = b"\x89PNG\r\n\x1a\n";
+ let search_start = Self::THUMB_OFF;
+ let idx = self.raw[search_start..]
+ .windows(png_magic.len())
+ .position(|w| w == png_magic)?;
+ let abs_idx = search_start + idx;
+ let iend = self.raw[abs_idx..].windows(4).position(|w| w == b"IEND")?;
+ let abs_iend = abs_idx + iend;
+ Some(self.raw[abs_idx..(abs_iend + 12).min(self.raw.len())].to_vec())
+ }
+
+ fn file_table(&self) -> Vec {
+ let ft_block = (self.raw[0x37E] as usize)
+ | ((self.raw[0x37F] as usize) << 8)
+ | ((self.raw[0x380] as usize) << 16);
+ let blk_data = self.read_block(ft_block);
+ let mut entries = Vec::new();
+ for i in 0..64 {
+ let base = i * 64;
+ if base + 64 > blk_data.len() {
+ break;
+ }
+ let e = &blk_data[base..base + 64];
+ let name_len = (e[0x28] & 0x3F) as usize;
+ if name_len == 0 {
+ continue;
+ }
+ if (e[0x28] >> 6) & 0x02 != 0 {
+ continue;
+ }
+ let name: String = e[..name_len].iter().map(|&b| b as char).collect();
+ let start_block = (e[0x2F] as usize)
+ | ((e[0x30] as usize) << 8)
+ | ((e[0x31] as usize) << 16);
+ let file_size =
+ u32::from_be_bytes([e[0x34], e[0x35], e[0x36], e[0x37]]) as usize;
+ if !name.is_empty() && file_size > 0 {
+ entries.push(StfsEntry {
+ name,
+ start_block,
+ size: file_size,
+ });
+ }
+ }
+ entries
+ }
+
+ fn extract_savegame_dat(&self) -> Option> {
+ for cand in &["savegame.dat", "SAVEGAME.DAT"] {
+ for entry in self.file_table() {
+ if entry.name.to_lowercase() == cand.to_lowercase() {
+ return Some(stfs_read_file(
+ &self.raw,
+ entry.start_block,
+ entry.size,
+ self.table_shift,
+ ));
+ }
+ }
+ }
+ None
+ }
+}
+
+fn parse_region_filename(name: &str) -> Option<(&'static str, i32, i32)> {
+ let n = name.to_lowercase().replace('\\', "/");
+ if let Some(rest) = n.strip_prefix("dim1/r.") {
+ if let Some(coords) = rest.strip_suffix(".mcr") {
+ let dot_pos = coords.find('.')?;
+ let x = coords[..dot_pos].parse().ok()?;
+ let z = coords[dot_pos + 1..].parse().ok()?;
+ return Some(("end", x, z));
+ }
+ }
+ if let Some(rest) = n.strip_prefix("dim-1r.") {
+ if let Some(coords) = rest.strip_suffix(".mcr") {
+ let dot_pos = coords.find('.')?;
+ let x = coords[..dot_pos].parse().ok()?;
+ let z = coords[dot_pos + 1..].parse().ok()?;
+ return Some(("nether", x, z));
+ }
+ }
+ if let Some(rest) = n.strip_prefix("r.") {
+ if let Some(coords) = rest.strip_suffix(".mcr") {
+ let dot_pos = coords.find('.')?;
+ let x = coords[..dot_pos].parse().ok()?;
+ let z = coords[dot_pos + 1..].parse().ok()?;
+ return Some(("overworld", x, z));
+ }
+ }
+ None
+}
+
+fn spawn_sig(axis: char) -> [u8; 9] {
+ let mut sig = [0u8; 9];
+ sig[0] = 0x03;
+ sig[1] = 0x00;
+ sig[2] = 0x06;
+ sig[3..9].copy_from_slice(format!("Spawn{}", axis).as_bytes());
+ sig
+}
+
+fn read_spawn(level_dat: &[u8]) -> Option<(i32, i32, i32)> {
+ let mut coords = Vec::new();
+ for axis in ['X', 'Y', 'Z'] {
+ let sig = spawn_sig(axis);
+ let idx = level_dat
+ .windows(sig.len())
+ .position(|w| w == sig)?;
+ let val_off = idx + sig.len();
+ if val_off + 4 > level_dat.len() {
+ return None;
+ }
+ coords.push(i32::from_be_bytes([
+ level_dat[val_off],
+ level_dat[val_off + 1],
+ level_dat[val_off + 2],
+ level_dat[val_off + 3],
+ ]));
+ }
+ Some((coords[0], coords[1], coords[2]))
+}
+
+fn patch_spawn(level_dat: &mut [u8], x: i32, y: i32, z: i32) {
+ for (axis, val) in [('X', x), ('Y', y), ('Z', z)] {
+ let sig = spawn_sig(axis);
+ if let Some(idx) = level_dat.windows(sig.len()).position(|w| w == sig) {
+ let val_off = idx + sig.len();
+ if val_off + 4 <= level_dat.len() {
+ level_dat[val_off..val_off + 4].copy_from_slice(&val.to_be_bytes());
+ }
+ }
+ }
+}
+
+fn read_player_pos(player_dat: &[u8]) -> Option<(f64, f64, f64)> {
+ let idx = player_dat
+ .windows(PLAYER_POS_SIG.len())
+ .position(|w| w == PLAYER_POS_SIG)?;
+ let p = idx + PLAYER_POS_SIG.len();
+ if p + 24 > player_dat.len() {
+ return None;
+ }
+ let x = f64::from_be_bytes(player_dat[p..p + 8].try_into().ok()?);
+ let y = f64::from_be_bytes(player_dat[p + 8..p + 16].try_into().ok()?);
+ let z = f64::from_be_bytes(player_dat[p + 16..p + 24].try_into().ok()?);
+ Some((x, y, z))
+}
+
+fn patch_player_pos(player_dat: &mut [u8], x: f64, y: f64, z: f64) {
+ if let Some(idx) = player_dat
+ .windows(PLAYER_POS_SIG.len())
+ .position(|w| w == PLAYER_POS_SIG)
+ {
+ let p = idx + PLAYER_POS_SIG.len();
+ if p + 24 <= player_dat.len() {
+ player_dat[p..p + 8].copy_from_slice(&x.to_be_bytes());
+ player_dat[p + 8..p + 16].copy_from_slice(&y.to_be_bytes());
+ player_dat[p + 16..p + 24].copy_from_slice(&z.to_be_bytes());
+ }
+ }
+}
+
+fn compress_rle(data: &[u8]) -> Vec {
+ let mut out = Vec::new();
+ let n = data.len();
+ let mut i = 0;
+ while i < n {
+ let b = data[i];
+ let mut run = 1usize;
+ while i + run < n && data[i + run] == b && run < 256 {
+ run += 1;
+ }
+ if b == 0xFF {
+ if run <= 3 {
+ out.push(0xFF);
+ out.push((run - 1) as u8);
+ } else {
+ out.push(0xFF);
+ out.push((run - 1) as u8);
+ out.push(0xFF);
+ }
+ } else if run < 4 {
+ out.extend(std::iter::repeat(b).take(run));
+ } else {
+ out.push(0xFF);
+ out.push((run - 1) as u8);
+ out.push(b);
+ }
+ i += run;
+ }
+ out
+}
+
+fn build_empty_chunk_nbt(chunk_x: i32, chunk_z: i32) -> Vec {
+ let mut out = Vec::new();
+ out.extend_from_slice(b"\x0a\x00\x00");
+ out.extend_from_slice(b"\x0a\x00\x05Level");
+ out.extend_from_slice(b"\x07\x00\x06Blocks");
+ out.extend_from_slice(&32768i32.to_be_bytes());
+ out.extend(std::iter::repeat(0u8).take(32768));
+ out.extend_from_slice(b"\x07\x00\x04Data");
+ out.extend_from_slice(&16384i32.to_be_bytes());
+ out.extend(std::iter::repeat(0u8).take(16384));
+ out.extend_from_slice(b"\x07\x00\x08SkyLight");
+ out.extend_from_slice(&16384i32.to_be_bytes());
+ out.extend(std::iter::repeat(0xffu8).take(16384));
+ out.extend_from_slice(b"\x07\x00\x0aBlockLight");
+ out.extend_from_slice(&16384i32.to_be_bytes());
+ out.extend(std::iter::repeat(0u8).take(16384));
+ out.extend_from_slice(b"\x07\x00\x09HeightMap");
+ out.extend_from_slice(&256i32.to_be_bytes());
+ out.extend(std::iter::repeat(0u8).take(256));
+ out.extend_from_slice(b"\x03\x00\x04xPos");
+ out.extend_from_slice(&chunk_x.to_be_bytes());
+ out.extend_from_slice(b"\x03\x00\x04zPos");
+ out.extend_from_slice(&chunk_z.to_be_bytes());
+ out.extend_from_slice(b"\x04\x00\x0aLastUpdate");
+ out.extend_from_slice(&0i64.to_be_bytes());
+ out.extend_from_slice(b"\x01\x00\x10TerrainPopulated\x01");
+ out.extend_from_slice(b"\x09\x00\x08Entities\x0a\x00\x00\x00\x00");
+ out.extend_from_slice(b"\x09\x00\x0cTileEntities\x0a\x00\x00\x00\x00");
+ out.push(0x00);
+ out.push(0x00);
+ out
+}
+
+fn find_safe_spawn(
+ dropped: &HashSet<(i32, i32)>,
+ orig: (i32, i32, i32),
+) -> Option<(i32, i32, i32)> {
+ if dropped.is_empty() {
+ return None;
+ }
+ let (sx, sy, sz) = orig;
+ let sc_x = sx >> 4;
+ let sc_z = sz >> 4;
+ let too_close = |cx: i32, cz: i32| -> bool {
+ dropped
+ .iter()
+ .any(|&(dx, dz)| (cx - dx).abs().max((cz - dz).abs()) < 12)
+ };
+ if !too_close(sc_x, sc_z) {
+ return None;
+ }
+ for radius in 1..200i32 {
+ for dz in -radius..=radius {
+ for dx in -radius..=radius {
+ if dx.abs().max(dz.abs()) != radius {
+ continue;
+ }
+ let cx = sc_x + dx;
+ let cz = sc_z + dz;
+ if !too_close(cx, cz) {
+ return Some((cx * 16 + 8, sy, cz * 16 + 8));
+ }
+ }
+ }
+ }
+ None
+}
+
+fn lzxd_window_from_bits(bits: usize) -> Option {
+ match bits {
+ 15 => Some(lzxd::WindowSize::KB32),
+ 16 => Some(lzxd::WindowSize::KB64),
+ 17 => Some(lzxd::WindowSize::KB128),
+ 18 => Some(lzxd::WindowSize::KB256),
+ 19 => Some(lzxd::WindowSize::KB512),
+ 20 => Some(lzxd::WindowSize::MB1),
+ 21 => Some(lzxd::WindowSize::MB2),
+ _ => None,
+ }
+}
+
+fn try_lzxd(lzx_raw: &[u8], output_size: usize, window: lzxd::WindowSize) -> Option> {
+ let mut lzxd = lzxd::Lzxd::new(window);
+ lzxd.decompress_next(lzx_raw, output_size)
+ .ok()
+ .map(|slice| slice.to_vec())
+}
+
+fn decompress_region_chunk(xbox_data: &[u8]) -> Result, String> {
+ let hi = xbox_data[0];
+ let (output_size, _src_sz, lzx_raw) = if hi == 0xFF {
+ let output_size =
+ ((xbox_data[1] as usize) << 8) | xbox_data[2] as usize;
+ let src_sz =
+ ((xbox_data[3] as usize) << 8) | xbox_data[4] as usize;
+ let lzx_raw = &xbox_data[5..(5 + src_sz).min(xbox_data.len())];
+ (output_size, src_sz, lzx_raw)
+ } else {
+ let src_sz =
+ ((hi as usize) << 8) | xbox_data[1] as usize;
+ let lzx_raw = &xbox_data[2..(2 + src_sz).min(xbox_data.len())];
+ (LZX_BLOCK_SIZE, src_sz, lzx_raw)
+ };
+
+ if let Some(out) = try_lzxd(lzx_raw, output_size, lzxd::WindowSize::KB128) {
+ return Ok(out);
+ }
+ for w in [16, 15, 18, 19, 20, 21] {
+ if let Some(ws) = lzxd_window_from_bits(w) {
+ if let Some(out) = try_lzxd(lzx_raw, output_size, ws) {
+ return Ok(out);
+ }
+ }
+ }
+ Err("LZX decode failed (all window sizes rejected the chunk)".to_string())
+}
+
+fn convert_region(
+ data: &[u8],
+ dropped_slots: &mut Vec,
+ region_coords: Option<(i32, i32)>,
+) -> Result, String> {
+ let sect = 4096usize;
+ let mut buf = data.to_vec();
+ for i in 0..REGION_SECT_COUNT * 2 {
+ if (i + 1) * 4 <= buf.len() {
+ s32(&mut buf, i * 4);
+ }
+ }
+ let mut chunk_positions: HashMap = HashMap::new();
+ for slot in 0..REGION_SECT_COUNT {
+ if (slot + 1) * 4 > buf.len() {
+ break;
+ }
+ let off = u32::from_le_bytes([
+ buf[slot * 4],
+ buf[slot * 4 + 1],
+ buf[slot * 4 + 2],
+ buf[slot * 4 + 3],
+ ]) as usize;
+ if off == 0 {
+ continue;
+ }
+ let sn = (off >> 8) & 0xFFFFFF;
+ let count = off & 0xFF;
+ if sn < 2 {
+ continue;
+ }
+ let fo = sn * sect;
+ if fo + 8 > data.len() {
+ continue;
+ }
+ chunk_positions.insert(fo, (slot, sn, count));
+ }
+ if chunk_positions.is_empty() {
+ return Ok(buf);
+ }
+ let mut new_buf = vec![0u8; buf.len()];
+ let copy_end = (sect * 2).min(buf.len()).min(new_buf.len());
+ new_buf[..copy_end].copy_from_slice(&buf[..copy_end]);
+ let mut next_sector = 2usize;
+ let mut sorted_fos: Vec = chunk_positions.keys().copied().collect();
+ sorted_fos.sort();
+ for fo in sorted_fos {
+ let (slot, _sn, _count) = chunk_positions[&fo];
+ let raw_comp_len = u32::from_be_bytes([
+ data[fo],
+ data[fo + 1],
+ data[fo + 2],
+ data[fo + 3],
+ ]);
+ let raw_decomp_len = u32::from_be_bytes([
+ data[fo + 4],
+ data[fo + 5],
+ data[fo + 6],
+ data[fo + 7],
+ ]);
+ let use_rle = (raw_comp_len & 0x80000000) != 0;
+ let comp_len = (raw_comp_len & 0x7FFFFFFF) as usize;
+ let decomp_len = raw_decomp_len as usize;
+ if comp_len == 0 || fo + 8 + comp_len > data.len() {
+ continue;
+ }
+ let xbox_data = &data[fo + 8..fo + 8 + comp_len];
+ let rle_data = match decompress_region_chunk(xbox_data) {
+ Ok(d) => d,
+ Err(_) => {
+ dropped_slots.push(slot);
+ if let Some((rx, rz)) = region_coords {
+ let cx = rx * 32 + (slot % 32) as i32;
+ let cz = rz * 32 + (slot / 32) as i32;
+ let synth_nbt = build_empty_chunk_nbt(cx, cz);
+ let synth_rle = compress_rle(&synth_nbt);
+ let mut synth_encoder =
+ flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::new(6));
+ let _ = synth_encoder.write_all(&synth_rle);
+ let synth_zlib = synth_encoder.finish().unwrap_or_default();
+ let new_comp_len = synth_zlib.len();
+ let needed = (8 + new_comp_len + sect - 1) / sect;
+ let dest_off = next_sector * sect;
+ while dest_off + 8 + new_comp_len > new_buf.len() {
+ new_buf.extend(std::iter::repeat(0u8).take(sect));
+ }
+ new_buf[dest_off..dest_off + 4]
+ .copy_from_slice(&(new_comp_len as u32 | 0x80000000).to_le_bytes());
+ new_buf[dest_off + 4..dest_off + 8]
+ .copy_from_slice(&(synth_nbt.len() as u32).to_le_bytes());
+ new_buf[dest_off + 8..dest_off + 8 + new_comp_len]
+ .copy_from_slice(&synth_zlib);
+ new_buf[slot * 4..slot * 4 + 4]
+ .copy_from_slice(&((next_sector << 8) | needed).to_le_bytes());
+ next_sector += needed;
+ } else {
+ new_buf[slot * 4..slot * 4 + 4].copy_from_slice(&0u32.to_le_bytes());
+ }
+ continue;
+ }
+ };
+ let zlib_data = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::new(6));
+ let mut encoder = zlib_data;
+ encoder.write_all(&rle_data).map_err(|e| e.to_string())?;
+ let zlib_data = encoder.finish().map_err(|e| e.to_string())?;
+ let new_comp_len = zlib_data.len();
+ let needed = (8 + new_comp_len + sect - 1) / sect;
+ let dest_off = next_sector * sect;
+ while dest_off + 8 + new_comp_len > new_buf.len() {
+ new_buf.extend(std::iter::repeat(0u8).take(sect));
+ }
+ let comp_flag = if use_rle {
+ new_comp_len as u32 | 0x80000000
+ } else {
+ new_comp_len as u32
+ };
+ new_buf[dest_off..dest_off + 4].copy_from_slice(&comp_flag.to_le_bytes());
+ new_buf[dest_off + 4..dest_off + 8].copy_from_slice(&(decomp_len as u32).to_le_bytes());
+ new_buf[dest_off + 8..dest_off + 8 + new_comp_len].copy_from_slice(&zlib_data);
+ let new_off = (next_sector << 8) | needed;
+ new_buf[slot * 4..slot * 4 + 4].copy_from_slice(&(new_off as u32).to_le_bytes());
+ next_sector += needed;
+ }
+ let end = (next_sector * sect).min(new_buf.len());
+ new_buf.truncate(end);
+ Ok(new_buf)
+}
+
+fn convert_region_ps3(data: &[u8]) -> Result, String> {
+ let sect = 4096usize;
+ let mut buf = data.to_vec();
+ for i in 0..REGION_SECT_COUNT * 2 {
+ if (i + 1) * 4 <= buf.len() {
+ s32(&mut buf, i * 4);
+ }
+ }
+ let mut chunk_positions: HashMap = HashMap::new();
+ for slot in 0..REGION_SECT_COUNT {
+ if (slot + 1) * 4 > buf.len() {
+ break;
+ }
+ let off = u32::from_le_bytes([
+ buf[slot * 4],
+ buf[slot * 4 + 1],
+ buf[slot * 4 + 2],
+ buf[slot * 4 + 3],
+ ]) as usize;
+ if off == 0 {
+ continue;
+ }
+ let sn = (off >> 8) & 0xFFFFFF;
+ let count = off & 0xFF;
+ if sn < 2 {
+ continue;
+ }
+ let fo = sn * sect;
+ if fo + 8 > data.len() {
+ continue;
+ }
+ chunk_positions.insert(fo, (slot, sn, count));
+ }
+ if chunk_positions.is_empty() {
+ return Ok(buf);
+ }
+ let mut new_buf = vec![0u8; buf.len()];
+ let copy_end = (sect * 2).min(buf.len()).min(new_buf.len());
+ new_buf[..copy_end].copy_from_slice(&buf[..copy_end]);
+ let mut next_sector = 2usize;
+ let mut sorted_fos: Vec = chunk_positions.keys().copied().collect();
+ sorted_fos.sort();
+ for fo in sorted_fos {
+ let (slot, _sn, _count) = chunk_positions[&fo];
+ let raw_comp_len = u32::from_be_bytes([
+ data[fo],
+ data[fo + 1],
+ data[fo + 2],
+ data[fo + 3],
+ ]);
+ let raw_decomp_len = u32::from_be_bytes([
+ data[fo + 4],
+ data[fo + 5],
+ data[fo + 6],
+ data[fo + 7],
+ ]);
+ let use_rle = (raw_comp_len & 0x80000000) != 0;
+ let comp_len = (raw_comp_len & 0x7FFFFFFF) as usize;
+ let decomp_len = raw_decomp_len as usize;
+ if comp_len == 0 || fo + 8 + comp_len > data.len() {
+ continue;
+ }
+ let chunk_body = &data[fo + 8..fo + 8 + comp_len];
+ if chunk_body.len() < 5 {
+ new_buf[slot * 4..slot * 4 + 4].copy_from_slice(&0u32.to_le_bytes());
+ continue;
+ }
+ let mut decoder = flate2::read::DeflateDecoder::new(&chunk_body[4..]);
+ let mut rle_data = Vec::new();
+ if decoder.read_to_end(&mut rle_data).is_err() {
+ new_buf[slot * 4..slot * 4 + 4].copy_from_slice(&0u32.to_le_bytes());
+ continue;
+ }
+ let mut encoder =
+ flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::new(6));
+ encoder.write_all(&rle_data).map_err(|e| e.to_string())?;
+ let zlib_data = encoder.finish().map_err(|e| e.to_string())?;
+ let new_comp_len = zlib_data.len();
+ let needed = (8 + new_comp_len + sect - 1) / sect;
+ let dest_off = next_sector * sect;
+ while dest_off + 8 + new_comp_len > new_buf.len() {
+ new_buf.extend(std::iter::repeat(0u8).take(sect));
+ }
+ let comp_flag = if use_rle {
+ new_comp_len as u32 | 0x80000000
+ } else {
+ new_comp_len as u32
+ };
+ new_buf[dest_off..dest_off + 4].copy_from_slice(&comp_flag.to_le_bytes());
+ new_buf[dest_off + 4..dest_off + 8].copy_from_slice(&(decomp_len as u32).to_le_bytes());
+ new_buf[dest_off + 8..dest_off + 8 + new_comp_len].copy_from_slice(&zlib_data);
+ let new_off = (next_sector << 8) | needed;
+ new_buf[slot * 4..slot * 4 + 4].copy_from_slice(&(new_off as u32).to_le_bytes());
+ next_sector += needed;
+ }
+ let end = (next_sector * sect).min(new_buf.len());
+ new_buf.truncate(end);
+ Ok(new_buf)
+}
+
+fn decompress_xmemcompress(dat: &[u8]) -> Result, String> {
+ if dat.len() < 8 {
+ return Err("savegame.dat too small".to_string());
+ }
+ let meta_off = u32::from_be_bytes([dat[0], dat[1], dat[2], dat[3]]) as usize;
+ if meta_off > dat.len() {
+ return Err("invalid metadata offset".to_string());
+ }
+ let lzx_stream = &dat[8..meta_off];
+ if lzx_stream.len() < 4 {
+ return Err("LZX stream too small".to_string());
+ }
+ let uncomp_total = u32::from_be_bytes([
+ lzx_stream[0],
+ lzx_stream[1],
+ lzx_stream[2],
+ lzx_stream[3],
+ ]) as usize;
+ let mut lzxd = lzxd::Lzxd::new(lzxd::WindowSize::KB128);
+ let mut output = Vec::with_capacity(uncomp_total);
+ let mut pos = 4;
+ while pos < lzx_stream.len() && output.len() < uncomp_total {
+ let hi = lzx_stream[pos];
+ let (src_sz, dst_sz, header_len) = if hi == 0xFF {
+ if pos + 5 > lzx_stream.len() {
+ break;
+ }
+ let dst_sz = ((lzx_stream[pos + 1] as usize) << 8) | lzx_stream[pos + 2] as usize;
+ let src_sz = ((lzx_stream[pos + 3] as usize) << 8) | lzx_stream[pos + 4] as usize;
+ (src_sz, dst_sz, 5)
+ } else {
+ if pos + 2 > lzx_stream.len() {
+ break;
+ }
+ let src_sz = ((hi as usize) << 8) | lzx_stream[pos + 1] as usize;
+ (src_sz, LZX_BLOCK_SIZE, 2)
+ };
+ pos += header_len;
+ if src_sz == 0 || dst_sz == 0 {
+ break;
+ }
+ if pos + src_sz > lzx_stream.len() {
+ break;
+ }
+ let lzx_raw = &lzx_stream[pos..pos + src_sz];
+ let remaining = uncomp_total - output.len();
+ let out_sz = dst_sz.min(remaining);
+ match lzxd.decompress_next(lzx_raw, out_sz) {
+ Ok(decompressed) => output.extend_from_slice(decompressed),
+ Err(e) => {
+ return Err(format!("LZX decompression failed at offset {}: {}", pos, e))
+ }
+ }
+ pos += src_sz;
+ }
+ Ok(output)
+}
+
+fn patch_level_name(nbt: &[u8], new_name: &str) -> Vec {
+ let idx = match nbt
+ .windows(LEVELNAME_SIG.len())
+ .position(|w| w == LEVELNAME_SIG)
+ {
+ Some(i) => i,
+ None => return nbt.to_vec(),
+ };
+ let str_off = idx + LEVELNAME_SIG.len();
+ if str_off + 2 > nbt.len() {
+ return nbt.to_vec();
+ }
+ let old_len = u16::from_be_bytes([nbt[str_off], nbt[str_off + 1]]) as usize;
+ let end = str_off + 2 + old_len;
+ let mut new_val = new_name.as_bytes().to_vec();
+ if new_val.len() > 0xFFFF {
+ new_val.truncate(0xFFFF);
+ }
+ let mut payload = Vec::with_capacity(2 + new_val.len());
+ payload.extend_from_slice(&(new_val.len() as u16).to_be_bytes());
+ payload.extend_from_slice(&new_val);
+ let mut out = Vec::with_capacity(str_off + payload.len() + nbt.len().saturating_sub(end));
+ out.extend_from_slice(&nbt[..str_off]);
+ out.extend_from_slice(&payload);
+ if end < nbt.len() {
+ out.extend_from_slice(&nbt[end..]);
+ }
+ out
+}
+
+fn parse_param_sfo(path: &Path) -> HashMap {
+ let data = match fs::read(path) {
+ Ok(d) => d,
+ Err(_) => return HashMap::new(),
+ };
+ if data.len() < 0x14 || &data[..4] != b"\x00PSF" {
+ return HashMap::new();
+ }
+ let key_tbl = u32::from_le_bytes(data[8..12].try_into().unwrap()) as usize;
+ let data_tbl = u32::from_le_bytes(data[12..16].try_into().unwrap()) as usize;
+ let entries = u32::from_le_bytes(data[16..20].try_into().unwrap()) as usize;
+ let mut result = HashMap::new();
+ for i in 0..entries {
+ let e = 0x14 + i * 16;
+ if e + 16 > data.len() {
+ break;
+ }
+ let key_off = u16::from_le_bytes([data[e], data[e + 1]]) as usize;
+ let fmt = u16::from_le_bytes([data[e + 2], data[e + 3]]);
+ let dlen = u16::from_le_bytes([data[e + 4], data[e + 5]]) as usize;
+ let doff = u32::from_le_bytes(data[e + 8..e + 12].try_into().unwrap()) as usize;
+ let ka = key_tbl + key_off;
+ let ke = data[ka..]
+ .iter()
+ .position(|&b| b == 0)
+ .map(|p| ka + p)
+ .unwrap_or(data.len());
+ let key = String::from_utf8_lossy(&data[ka..ke]).to_string();
+ let da = data_tbl + doff;
+ let val = if (fmt == SFO_FMT_UTF8_RAW || fmt == SFO_FMT_UTF8_STR) && da + dlen <= data.len()
+ {
+ let raw = &data[da..da + dlen];
+ let null_pos = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
+ String::from_utf8_lossy(&raw[..null_pos]).to_string()
+ } else if fmt == SFO_FMT_INT32 && da + 4 <= data.len() {
+ u32::from_le_bytes(data[da..da + 4].try_into().unwrap()).to_string()
+ } else {
+ continue;
+ };
+ result.insert(key, val);
+ }
+ result
+}
+
+fn looks_like_4j_header(data: &[u8]) -> bool {
+ if data.len() < 12 {
+ return false;
+ }
+ let (ho, ne, _ov, cv) = read_hdr_be(data);
+ if ho < 12 || ho as usize >= data.len() {
+ return false;
+ }
+ if data.len() - ho as usize != ne as usize * FILE_ENTRY_SIZE {
+ return false;
+ }
+ if ne == 0 || ne > 4096 {
+ return false;
+ }
+ if cv < 0 || cv > 20 {
+ return false;
+ }
+ true
+}
+
+fn convert_bin_to_win64(bin_path: &str, game_dir: &str) -> Result {
+ let raw = fs::read(bin_path).map_err(|e| format!("Failed to read file: {}", e))?;
+ let pkg = StfsPackage::new(raw)?;
+ let name = pkg.display_name();
+ let dat = pkg
+ .extract_savegame_dat()
+ .ok_or("savegame.dat not found in STFS package")?;
+ let decompressed = decompress_xmemcompress(&dat)?;
+ let (ho, ne, ov, cv) = read_hdr_be(&decompressed);
+ let entries = parse_ftable_be(&decompressed, ho, ne);
+ let mut file_blobs: Vec> = Vec::new();
+ let mut dropped_overworld: HashSet<(i32, i32)> = HashSet::new();
+ for e in &entries {
+ let fn_lower = e.filename.to_lowercase();
+ let s = e.start_offset as usize;
+ let l = e.length as usize;
+ let raw_file = if s + l <= decompressed.len() {
+ decompressed[s..s + l].to_vec()
+ } else {
+ Vec::new()
+ };
+ if fn_lower.ends_with(".mcr") && !raw_file.is_empty() {
+ let dim_info = parse_region_filename(&e.filename);
+ let region_xy = dim_info.map(|d| (d.1, d.2));
+ let mut slots = Vec::new();
+ let converted =
+ convert_region(&raw_file, &mut slots, region_xy)?;
+ if let Some(("overworld", rx, rz)) = dim_info {
+ for slot in slots {
+ let cx = rx * 32 + (slot % 32) as i32;
+ let cz = rz * 32 + (slot / 32) as i32;
+ dropped_overworld.insert((cx, cz));
+ }
+ }
+ file_blobs.push(converted);
+ } else {
+ file_blobs.push(raw_file);
+ }
+ }
+ if !dropped_overworld.is_empty() {
+ let mut new_spawn: Option<(i32, i32, i32)> = None;
+ let mut spawn: Option<(i32, i32, i32)> = None;
+ for (i, e) in entries.iter().enumerate() {
+ if e.filename.to_lowercase() != "level.dat" {
+ continue;
+ }
+ spawn = read_spawn(&file_blobs[i]);
+ if let Some(s) = spawn {
+ if let Some(ns) = find_safe_spawn(&dropped_overworld, s) {
+ let blob = &mut file_blobs[i];
+ patch_spawn(blob, ns.0, ns.1, ns.2);
+ new_spawn = Some(ns);
+ }
+ }
+ break;
+ }
+ let safe = new_spawn.or(spawn);
+ if let Some((sx, sy, sz)) = safe {
+ let _sx_chunk = sx >> 4;
+ let _sz_chunk = sz >> 4;
+ let too_close = |cx: i32, cz: i32| -> bool {
+ dropped_overworld
+ .iter()
+ .any(|&(dx, dz)| (cx - dx).abs().max((cz - dz).abs()) < 12)
+ };
+ let mut moved_players = 0i32;
+ for (i, e) in entries.iter().enumerate() {
+ if !e.filename.starts_with("players/") || !e.filename.ends_with(".dat") {
+ continue;
+ }
+ if let Some((px, _py, pz)) = read_player_pos(&file_blobs[i]) {
+ let cx = px as i32 / 16;
+ let cz = pz as i32 / 16;
+ if too_close(cx, cz) {
+ let blob = &mut file_blobs[i];
+ patch_player_pos(blob, sx as f64 + 0.5, sy as f64, sz as f64 + 0.5);
+ moved_players += 1;
+ }
+ }
+ }
+ if moved_players > 0 {
+ eprintln!("Relocated {} player(s) to safe spawn", moved_players);
+ }
+ }
+ }
+ let header_size = 12usize;
+ let mut body = Vec::new();
+ let mut new_entries: Vec = Vec::new();
+ let mut cursor = header_size;
+ for (i, e) in entries.iter().enumerate() {
+ let blob = &file_blobs[i];
+ new_entries.push(FileEntry {
+ filename: e.filename.clone(),
+ length: blob.len() as u32,
+ start_offset: cursor as u32,
+ last_mod: e.last_mod,
+ });
+ body.extend_from_slice(blob);
+ cursor += blob.len();
+ }
+ let new_fto = cursor;
+ let out_cv = if cv <= 9 { cv } else { 9 };
+ let mut header = Vec::with_capacity(header_size);
+ header.extend_from_slice(&(new_fto as u32).to_le_bytes());
+ header.extend_from_slice(&(entries.len() as u32).to_le_bytes());
+ header.extend_from_slice(&ov.to_le_bytes());
+ header.extend_from_slice(&out_cv.to_le_bytes());
+ let mut raw_le = Vec::with_capacity(header.len() + body.len() + ne as usize * FILE_ENTRY_SIZE);
+ raw_le.extend_from_slice(&header);
+ raw_le.extend_from_slice(&body);
+ for ne_entry in &new_entries {
+ let fn_bytes: Vec = ne_entry.filename.encode_utf16().map(|c| c.to_le()).collect();
+ let mut fn_padded = vec![0u16; 64];
+ for (j, &c) in fn_bytes.iter().take(64).enumerate() {
+ fn_padded[j] = c;
+ }
+ for &c in &fn_padded {
+ raw_le.extend_from_slice(&c.to_le_bytes());
+ }
+ raw_le.extend_from_slice(&ne_entry.length.to_le_bytes());
+ raw_le.extend_from_slice(&ne_entry.start_offset.to_le_bytes());
+ raw_le.extend_from_slice(&ne_entry.last_mod.to_le_bytes());
+ }
+ let mut encoder =
+ flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::new(6));
+ encoder.write_all(&raw_le).map_err(|e| e.to_string())?;
+ let compressed = encoder.finish().map_err(|e| e.to_string())?;
+ let mut win64 = Vec::with_capacity(8 + compressed.len());
+ win64.extend_from_slice(&0u32.to_le_bytes());
+ win64.extend_from_slice(&(raw_le.len() as u32).to_le_bytes());
+ win64.extend_from_slice(&compressed);
+ let folder = sanitise(&name);
+ let dst = Path::new(game_dir).join(&folder);
+ fs::create_dir_all(&dst).map_err(|e| format!("Failed to create output directory: {}", e))?;
+ fs::write(dst.join("saveData.ms"), &win64)
+ .map_err(|e| format!("Failed to write saveData.ms: {}", e))?;
+ if let Some(thumb) = pkg.thumbnail() {
+ let thumb_dir = dst.join("thumbnails");
+ let _ = fs::create_dir_all(&thumb_dir);
+ let _ = fs::write(thumb_dir.join("thumbData.png"), &thumb);
+ }
+ Ok(dst.to_string_lossy().to_string())
+}
+
+fn convert_ps3_to_win64(ps3_save_dir: &str, game_dir: &str) -> Result {
+ let src = Path::new(ps3_save_dir);
+ if !src.is_dir() {
+ return Err(format!(
+ "'{}' is not a folder",
+ src.display()
+ ));
+ }
+ let gamedata_path = src.join("GAMEDATA");
+ if !gamedata_path.exists() {
+ return Err(format!(
+ "GAMEDATA not found in {}",
+ src.file_name().unwrap_or_default().to_string_lossy()
+ ));
+ }
+ let sfo = parse_param_sfo(&src.join("PARAM.SFO"));
+ let save_title = sfo
+ .get("SUB_TITLE")
+ .cloned()
+ .unwrap_or_else(|| {
+ src.file_name()
+ .unwrap_or_default()
+ .to_string_lossy()
+ .to_string()
+ });
+ let decompressed =
+ fs::read(&gamedata_path).map_err(|e| format!("Failed to read GAMEDATA: {}", e))?;
+ if !looks_like_4j_header(&decompressed) {
+ return Err("GAMEDATA does not look like a valid Minecraft PS3 save".to_string());
+ }
+ let (ho, ne, ov, _cv) = read_hdr_be(&decompressed);
+ let entries = parse_ftable_be(&decompressed, ho, ne);
+ let mut file_blobs: Vec> = Vec::new();
+ for e in &entries {
+ let fn_lower = e.filename.to_lowercase();
+ let s = e.start_offset as usize;
+ let l = e.length as usize;
+ let mut raw_file = if s + l <= decompressed.len() {
+ decompressed[s..s + l].to_vec()
+ } else {
+ Vec::new()
+ };
+ if fn_lower.ends_with(".mcr") && !raw_file.is_empty() {
+ raw_file = convert_region_ps3(&raw_file)?;
+ } else if fn_lower == "level.dat" && !save_title.is_empty() && !raw_file.is_empty() {
+ raw_file = patch_level_name(&raw_file, &save_title);
+ }
+ file_blobs.push(raw_file);
+ }
+ let header_size = 12usize;
+ let mut body = Vec::new();
+ let mut new_entries: Vec = Vec::new();
+ let mut cursor = header_size;
+ for (i, e) in entries.iter().enumerate() {
+ let blob = &file_blobs[i];
+ new_entries.push(FileEntry {
+ filename: e.filename.clone(),
+ length: blob.len() as u32,
+ start_offset: cursor as u32,
+ last_mod: e.last_mod,
+ });
+ body.extend_from_slice(blob);
+ cursor += blob.len();
+ }
+ let new_fto = cursor;
+ let mut raw_le = Vec::with_capacity(header_size + body.len() + ne as usize * FILE_ENTRY_SIZE);
+ raw_le.extend_from_slice(&(new_fto as u32).to_le_bytes());
+ raw_le.extend_from_slice(&ne.to_le_bytes());
+ raw_le.extend_from_slice(&ov.to_le_bytes());
+ raw_le.extend_from_slice(&9i16.to_le_bytes());
+ raw_le.extend_from_slice(&body);
+ for ne_entry in &new_entries {
+ let fn_bytes: Vec = ne_entry.filename.encode_utf16().map(|c| c.to_le()).collect();
+ let mut fn_padded = vec![0u16; 64];
+ for (j, &c) in fn_bytes.iter().take(64).enumerate() {
+ fn_padded[j] = c;
+ }
+ for &c in &fn_padded {
+ raw_le.extend_from_slice(&c.to_le_bytes());
+ }
+ raw_le.extend_from_slice(&ne_entry.length.to_le_bytes());
+ raw_le.extend_from_slice(&ne_entry.start_offset.to_le_bytes());
+ raw_le.extend_from_slice(&ne_entry.last_mod.to_le_bytes());
+ }
+ let mut encoder =
+ flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::new(6));
+ encoder.write_all(&raw_le).map_err(|e| e.to_string())?;
+ let compressed = encoder.finish().map_err(|e| e.to_string())?;
+ let mut win64 = Vec::with_capacity(8 + compressed.len());
+ win64.extend_from_slice(&0u32.to_le_bytes());
+ win64.extend_from_slice(&(raw_le.len() as u32).to_le_bytes());
+ win64.extend_from_slice(&compressed);
+ let folder = sanitise(&save_title);
+ let dst = Path::new(game_dir).join(&folder);
+ fs::create_dir_all(&dst).map_err(|e| format!("Failed to create output directory: {}", e))?;
+ fs::write(dst.join("saveData.ms"), &win64)
+ .map_err(|e| format!("Failed to write saveData.ms: {}", e))?;
+ let thumb_path = src.join("THUMB");
+ if thumb_path.exists() {
+ if let Ok(thumb_bytes) = fs::read(&thumb_path) {
+ if thumb_bytes.len() >= 8 && thumb_bytes[..8] == *b"\x89PNG\r\n\x1a\n" {
+ let thumb_dir = dst.join("thumbnails");
+ let _ = fs::create_dir_all(&thumb_dir);
+ let _ = fs::write(thumb_dir.join("thumbData.png"), &thumb_bytes);
+ }
+ }
+ }
+ Ok(dst.to_string_lossy().to_string())
+}
#[tauri::command]
#[allow(non_snake_case)]
@@ -16,9 +1167,43 @@ pub async fn import_world(
.map_err(|e| format!("Failed to create output directory {:?}: {}", parent, e))?;
}
}
-
fs::copy(&input_path, &output_path)
.map_err(|e| format!("Failed to copy .ms file: {}", e))?;
-
- Ok(format!("World imported successfully!\nOutput: {}", output_path))
+ Ok(format!(
+ "World imported successfully!\nOutput: {}",
+ output_path
+ ))
+}
+
+#[tauri::command]
+#[allow(non_snake_case)]
+pub async fn import_lce_save(
+ _app: AppHandle,
+ input_path: String,
+ output_dir: String,
+) -> Result {
+ let path = Path::new(&input_path);
+ if path.is_file() {
+ let ext = path
+ .extension()
+ .unwrap_or_default()
+ .to_string_lossy()
+ .to_lowercase();
+ if ext == "ms" {
+ let dst = Path::new(&output_dir);
+ fs::create_dir_all(dst)
+ .map_err(|e| format!("Failed to create output directory: {}", e))?;
+ fs::copy(&input_path, dst.join("saveData.ms"))
+ .map_err(|e| format!("Failed to copy .ms file: {}", e))?;
+ return Ok(dst.to_string_lossy().to_string());
+ }
+ if ext == "bin" || ext == "" {
+ return convert_bin_to_win64(&input_path, &output_dir);
+ }
+ return Err(format!("Unrecognised file extension: .{}", ext));
+ }
+ if path.is_dir() && path.join("GAMEDATA").is_file() {
+ return convert_ps3_to_win64(&input_path, &output_dir);
+ }
+ Err("Input is not a valid LCE save (expected .bin file, .ms file, or PS3 folder with GAMEDATA)".to_string())
}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index e2852c6..25151d8 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -108,6 +108,7 @@ pub fn run() {
game::backup_instance,
game::restore_instance,
commands::console2lce::import_world,
+ commands::console2lce::import_lce_save,
stun::stun_discover,
relay::start_relay_proxy,
relay::start_host_relay,
diff --git a/src/components/modals/ImportWorldModal.tsx b/src/components/modals/ImportWorldModal.tsx
index 363053e..5cf5bb7 100644
--- a/src/components/modals/ImportWorldModal.tsx
+++ b/src/components/modals/ImportWorldModal.tsx
@@ -1,7 +1,6 @@
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { TauriService } from "../../services/TauriService";
-
export default function ImportWorldModal({
isOpen,
onClose,
@@ -20,7 +19,6 @@ export default function ImportWorldModal({
const [status, setStatus] = useState("");
const [error, setError] = useState("");
const [isImporting, setIsImporting] = useState(false);
-
useEffect(() => {
if (!isOpen) {
setStatus("");
@@ -29,19 +27,18 @@ export default function ImportWorldModal({
}
}, [isOpen]);
- const handleImport = async () => {
+ const handleImportMs = async () => {
if (!targetInstanceId) return;
playPressSound();
setIsImporting(true);
setError("");
setStatus("Selecting source...");
-
try {
- setStatus("Selecting LCE save folder or .ms file...");
- const picked = await TauriService.pickFile(
- "Select saveData.ms or GameHDD folder",
- ["*.ms", "*"],
- );
+ setStatus("Selecting saveData.ms file...");
+ const picked = await TauriService.pickFile("Select saveData.ms", [
+ "*.ms",
+ "*",
+ ]);
if (!picked) {
setIsImporting(false);
return;
@@ -64,6 +61,65 @@ export default function ImportWorldModal({
}
};
+ const handleImportXbox = async () => {
+ if (!targetInstanceId) return;
+ playPressSound();
+ setIsImporting(true);
+ setError("");
+ setStatus("Selecting source...");
+ try {
+ setStatus("Selecting Xbox 360 save (.bin)...");
+ const picked = await TauriService.pickFile(
+ "Select Xbox 360 Minecraft save",
+ ["*.bin", "*"],
+ );
+ if (!picked) {
+ setIsImporting(false);
+ return;
+ }
+ setStatus("Converting Xbox 360 save...");
+ const instancePath = await TauriService.getInstancePath(targetInstanceId);
+ const gameHdd = `${instancePath}/Windows64/GameHDD`;
+ await TauriService.importLceSave(picked, gameHdd);
+ setStatus(`Xbox 360 save converted into "${targetInstanceName}"!`);
+ setTimeout(() => {
+ onClose();
+ }, 2000);
+ } catch (e: unknown) {
+ setError(e instanceof Error ? e.message : String(e));
+ setStatus("");
+ setIsImporting(false);
+ }
+ };
+
+ const handleImportPs3 = async () => {
+ if (!targetInstanceId) return;
+ playPressSound();
+ setIsImporting(true);
+ setError("");
+ setStatus("Selecting source...");
+ try {
+ setStatus("Selecting PS3 save folder...");
+ const picked = await TauriService.pickFolder();
+ if (!picked) {
+ setIsImporting(false);
+ return;
+ }
+ setStatus("Converting PS3 save...");
+ const instancePath = await TauriService.getInstancePath(targetInstanceId);
+ const gameHdd = `${instancePath}/Windows64/GameHDD`;
+ await TauriService.importLceSave(picked, gameHdd);
+ setStatus(`PS3 save converted into "${targetInstanceName}"!`);
+ setTimeout(() => {
+ onClose();
+ }, 2000);
+ } catch (e: unknown) {
+ setError(e instanceof Error ? e.message : String(e));
+ setStatus("");
+ setIsImporting(false);
+ }
+ };
+
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") {
playBackSound();
@@ -104,9 +160,49 @@ export default function ImportWorldModal({
Import into:{" "}
{targetInstanceName}
-
- Select an existing LCE save (.ms file or GameHDD folder)
+
+
+ Import an existing .ms save file:
+
+
+
+ Convert an Xbox 360 or PS3 save:
+
+
+
+
+
{error && (
@@ -114,33 +210,20 @@ export default function ImportWorldModal({
)}
-
-
-
-
+
>
) : (
<>
diff --git a/src/services/TauriService.ts b/src/services/TauriService.ts
index cbfbc37..bc01e99 100644
--- a/src/services/TauriService.ts
+++ b/src/services/TauriService.ts
@@ -420,4 +420,11 @@ export class TauriService {
): Promise {
return invoke("import_world", { inputPath, outputPath });
}
+
+ static async importLceSave(
+ inputPath: string,
+ outputDir: string,
+ ): Promise {
+ return invoke("import_lce_save", { inputPath, outputDir });
+ }
}
From 159bb5705dc03aa0dce5c643100f8d5edb8dc2bd Mon Sep 17 00:00:00 2001
From: neoapps-dev
Date: Thu, 16 Jul 2026 13:47:28 +0300
Subject: [PATCH 2/4] ot(fix): moon edition
---
src/hooks/useGameManager.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/hooks/useGameManager.ts b/src/hooks/useGameManager.ts
index 793a2d6..056c40e 100644
--- a/src/hooks/useGameManager.ts
+++ b/src/hooks/useGameManager.ts
@@ -71,7 +71,7 @@ export const BASE_EDITIONS = [
id: "moon_edition",
name: "Minecraft: Moon Edition",
desc: "Galacticraft LCE port (Modded build!)",
- url: "https://github.com/blazin-blaze/moon-edition/releases/download/v1.0.1/moonEditionWindows64.zip",
+ url: "https://github.com/blazin-blaze/moon-edition/releases/latest/download/moonEditionWindows64.zip",
titleImage: "/images/minecraft_title_moon.png",
supportsSlimSkins: false,
logo: "/images/moonEdition.png",
@@ -660,4 +660,4 @@ export function useGameManager({
updateCustomization,
saveCustomPath,
};
-}
+}
\ No newline at end of file
From 9318d7d50f6e526c05c7020540a81fdf30bcbdbd Mon Sep 17 00:00:00 2001
From: neoapps-dev
Date: Fri, 17 Jul 2026 20:53:29 +0300
Subject: [PATCH 3/4] feat: initial support for java world conversion
---
.../src/commands/java2lce/block_mapping.rs | 1353 +++++++++++++
src-tauri/src/commands/java2lce/chunk.rs | 1690 +++++++++++++++++
src-tauri/src/commands/java2lce/level_dat.rs | 251 +++
src-tauri/src/commands/java2lce/mod.rs | 235 +++
src-tauri/src/commands/java2lce/nbt.rs | 496 +++++
src-tauri/src/commands/java2lce/payload.rs | 683 +++++++
src-tauri/src/commands/java2lce/region.rs | 803 ++++++++
src-tauri/src/commands/mod.rs | 1 +
src-tauri/src/lib.rs | 2 +
src-tauri/src/networking/relay.rs | 2 -
src/components/modals/ImportWorldModal.tsx | 92 +
src/services/TauriService.ts | 14 +
12 files changed, 5620 insertions(+), 2 deletions(-)
create mode 100644 src-tauri/src/commands/java2lce/block_mapping.rs
create mode 100644 src-tauri/src/commands/java2lce/chunk.rs
create mode 100644 src-tauri/src/commands/java2lce/level_dat.rs
create mode 100644 src-tauri/src/commands/java2lce/mod.rs
create mode 100644 src-tauri/src/commands/java2lce/nbt.rs
create mode 100644 src-tauri/src/commands/java2lce/payload.rs
create mode 100644 src-tauri/src/commands/java2lce/region.rs
diff --git a/src-tauri/src/commands/java2lce/block_mapping.rs b/src-tauri/src/commands/java2lce/block_mapping.rs
new file mode 100644
index 0000000..cee48fc
--- /dev/null
+++ b/src-tauri/src/commands/java2lce/block_mapping.rs
@@ -0,0 +1,1353 @@
+use std::collections::HashMap;
+use super::nbt::NbtCompound;
+#[derive(Clone, Copy, Default)]
+pub struct LegacyBlockState {
+ pub id: u8,
+ pub data: u8,
+}
+
+pub fn map_modern_block_state(compound: &NbtCompound) -> LegacyBlockState {
+ map_modern_block_state_inner(compound, None)
+}
+
+pub fn map_modern_block_state_with_context(
+ compound: &NbtCompound,
+ unknown_blocks: Option<&mut Vec>,
+) -> LegacyBlockState {
+ map_modern_block_state_inner(compound, unknown_blocks)
+}
+
+fn map_modern_block_state_inner(
+ compound: &NbtCompound,
+ mut unknown_blocks: Option<&mut Vec>,
+) -> LegacyBlockState {
+ let name_raw = compound.string("Name").unwrap_or("minecraft:air");
+ let name = name_raw.strip_prefix("minecraft:").unwrap_or(name_raw);
+ let properties = compound.compound("Properties");
+ if let Some(fluid) = try_map_fluid(name, properties) {
+ return fluid;
+ }
+ if let Some(slab) = try_map_slab(name, properties) {
+ return slab;
+ }
+ if let Some(directional) = try_map_directional(name, properties) {
+ return directional;
+ }
+ if let Some(flattened) = try_map_flattened_colored(name) {
+ return flattened;
+ }
+ if let Some(direct) = MODERN_DIRECT_MAP.get(name) {
+ return *direct;
+ }
+ if let Some(colored) = try_map_colored(name, properties) {
+ return colored;
+ }
+ if let Some(wood) = try_map_wood(name, properties) {
+ return wood;
+ }
+ if let Some(variant) = try_map_variant(name, properties) {
+ return variant;
+ }
+
+ if let Some(ref mut blocks) = unknown_blocks {
+ if !name.is_empty() && name != "air" && name != "cave_air" && name != "void_air" {
+ blocks.push(name.to_string());
+ }
+ }
+
+ LegacyBlockState { id: 0, data: 0 }
+}
+
+fn get_property<'a>(properties: Option<&'a NbtCompound>, name: &str) -> String {
+ properties
+ .and_then(|p| p.string(name))
+ .unwrap_or("")
+ .to_string()
+}
+
+fn get_bool_property(properties: Option<&NbtCompound>, name: &str) -> bool {
+ get_property(properties, name).to_lowercase() == "true"
+}
+
+fn get_int_property(properties: Option<&NbtCompound>, name: &str, default: i32) -> i32 {
+ get_property(properties, name)
+ .parse()
+ .unwrap_or(default)
+}
+
+fn try_map_fluid(name: &str, properties: Option<&NbtCompound>) -> Option {
+ if name != "water" && name != "lava" {
+ return None;
+ }
+ let level = get_int_property(properties, "level", 0).clamp(0, 15);
+ let is_source = level == 0;
+ let data = level as u8;
+ Some(if name == "water" {
+ LegacyBlockState {
+ id: if is_source { 9 } else { 8 },
+ data,
+ }
+ } else {
+ LegacyBlockState {
+ id: if is_source { 11 } else { 10 },
+ data,
+ }
+ })
+}
+
+fn try_map_directional(name: &str, properties: Option<&NbtCompound>) -> Option {
+ match name {
+ "ladder" => Some(LegacyBlockState {
+ id: 65,
+ data: map_ladder_facing(&get_property(properties, "facing")),
+ }),
+ "vine" => Some(LegacyBlockState {
+ id: 106,
+ data: map_vine_faces(properties),
+ }),
+ "lever" => {
+ let mut data =
+ map_lever_data(&get_property(properties, "face"), &get_property(properties, "facing"));
+ if get_bool_property(properties, "powered") {
+ data |= 8;
+ }
+ Some(LegacyBlockState { id: 69, data })
+ }
+ _ if name.ends_with("_button") => {
+ let id = if is_wood_family(name) { 143 } else { 77 };
+ let mut data = map_button_facing(&get_property(properties, "facing"));
+ if get_bool_property(properties, "powered") {
+ data |= 8;
+ }
+ Some(LegacyBlockState { id, data })
+ }
+ _ if name.ends_with("_fence_gate") => {
+ let mut data = map_fence_gate_facing(&get_property(properties, "facing"));
+ if get_bool_property(properties, "open") {
+ data |= 4;
+ }
+ Some(LegacyBlockState { id: 107, data })
+ }
+ _ if name.ends_with("_pressure_plate") => {
+ let id = match name {
+ "light_weighted_pressure_plate" => 147,
+ "heavy_weighted_pressure_plate" => 148,
+ _ if is_wood_family(name) => 72,
+ _ => 70,
+ };
+ let powered =
+ get_bool_property(properties, "powered") || get_int_property(properties, "power", 0) > 0;
+ Some(LegacyBlockState {
+ id,
+ data: if powered { 1 } else { 0 },
+ })
+ }
+ _ if name.ends_with("_door") || name == "iron_door" => {
+ let id = if name == "iron_door" { 71 } else { 64 };
+ let half = get_property(properties, "half");
+ let is_upper = half == "upper";
+ if is_upper {
+ let mut data = 8;
+ if get_property(properties, "hinge") == "right" {
+ data |= 1;
+ }
+ if get_bool_property(properties, "powered") {
+ data |= 2;
+ }
+ Some(LegacyBlockState { id, data })
+ } else {
+ let mut data = map_door_facing(&get_property(properties, "facing"));
+ if get_bool_property(properties, "open") {
+ data |= 4;
+ }
+ Some(LegacyBlockState { id, data })
+ }
+ }
+ _ if try_get_stairs_id(name).is_some() => {
+ let id = try_get_stairs_id(name).unwrap();
+ let mut data = map_stairs_facing(&get_property(properties, "facing"));
+ if get_property(properties, "half") == "top" {
+ data |= 4;
+ }
+ Some(LegacyBlockState { id, data })
+ }
+ _ if name.ends_with("trapdoor") => {
+ let mut data = map_trapdoor_facing(&get_property(properties, "facing"));
+ if get_bool_property(properties, "open") {
+ data |= 4;
+ }
+ if get_property(properties, "half") == "top" {
+ data |= 8;
+ }
+ Some(LegacyBlockState { id: 96, data })
+ }
+ "dispenser" | "dropper" => {
+ let id = if name == "dropper" { 158 } else { 23 };
+ let mut data = map_facing_data(&get_property(properties, "facing"));
+ if get_bool_property(properties, "triggered") {
+ data |= 8;
+ }
+ Some(LegacyBlockState { id, data })
+ }
+ "piston" | "sticky_piston" => {
+ let id = if name == "sticky_piston" { 29 } else { 33 };
+ let mut data = map_facing_data(&get_property(properties, "facing"));
+ if get_bool_property(properties, "extended") {
+ data |= 8;
+ }
+ Some(LegacyBlockState { id, data })
+ }
+ "piston_head" => {
+ let mut data = map_facing_data(&get_property(properties, "facing"));
+ if get_property(properties, "type") == "sticky" {
+ data |= 8;
+ }
+ Some(LegacyBlockState { id: 34, data })
+ }
+ "redstone_wire" => {
+ let data = get_int_property(properties, "power", 0).clamp(0, 15) as u8;
+ Some(LegacyBlockState { id: 55, data })
+ }
+ "repeater" => {
+ let id = if get_bool_property(properties, "powered") { 94 } else { 93 };
+ let dir = map_repeater_direction(&get_property(properties, "facing"));
+ let delay = get_int_property(properties, "delay", 1).clamp(1, 4);
+ Some(LegacyBlockState {
+ id,
+ data: dir | (((delay - 1) as u8) << 2),
+ })
+ }
+ "comparator" => {
+ let id = if get_bool_property(properties, "powered") { 150 } else { 149 };
+ let dir = map_repeater_direction(&get_property(properties, "facing"));
+ let mode = if get_property(properties, "mode") == "subtract" { 4 } else { 0 };
+ Some(LegacyBlockState {
+ id,
+ data: dir | mode,
+ })
+ }
+ "wall_torch" => Some(LegacyBlockState {
+ id: 50,
+ data: map_wall_torch_facing(&get_property(properties, "facing")),
+ }),
+ "redstone_wall_torch" => {
+ let id = if get_bool_property(properties, "lit") { 76 } else { 75 };
+ Some(LegacyBlockState {
+ id,
+ data: map_wall_torch_facing(&get_property(properties, "facing")),
+ })
+ }
+ "nether_wart" => Some(LegacyBlockState {
+ id: 115,
+ data: get_int_property(properties, "age", 0).clamp(0, 3) as u8,
+ }),
+ "wheat" => Some(LegacyBlockState {
+ id: 59,
+ data: get_int_property(properties, "age", 0).clamp(0, 7) as u8,
+ }),
+ "pumpkin_stem" | "attached_pumpkin_stem" => Some(LegacyBlockState {
+ id: 104,
+ data: if name == "attached_pumpkin_stem" {
+ 7
+ } else {
+ get_int_property(properties, "age", 0).clamp(0, 7) as u8
+ },
+ }),
+ "melon_stem" | "attached_melon_stem" => Some(LegacyBlockState {
+ id: 105,
+ data: if name == "attached_melon_stem" {
+ 7
+ } else {
+ get_int_property(properties, "age", 0).clamp(0, 7) as u8
+ },
+ }),
+ "cocoa" => {
+ let age = get_int_property(properties, "age", 0).clamp(0, 2) as u8;
+ let dir = map_repeater_direction(&get_property(properties, "facing"));
+ Some(LegacyBlockState {
+ id: 127,
+ data: (age << 2) | dir,
+ })
+ }
+ "hay_block" => Some(LegacyBlockState {
+ id: 170,
+ data: match get_property(properties, "axis").as_str() {
+ "x" => 4,
+ "z" => 8,
+ _ => 0,
+ },
+ }),
+ "quartz_pillar" => Some(LegacyBlockState {
+ id: 155,
+ data: match get_property(properties, "axis").as_str() {
+ "x" => 3,
+ "z" => 4,
+ _ => 2,
+ },
+ }),
+ "nether_portal" => Some(LegacyBlockState {
+ id: 90,
+ data: if get_property(properties, "axis") == "z" { 2 } else { 1 },
+ }),
+ _ => None,
+ }
+}
+
+fn try_map_slab(name: &str, properties: Option<&NbtCompound>) -> Option {
+ if !name.ends_with("_slab") {
+ return None;
+ }
+
+ let slab_type = get_property(properties, "type");
+ let is_top = slab_type == "top";
+ let is_double = slab_type == "double";
+ if let Some(wood_variant) = get_wood_slab_variant(name) {
+ return Some(if is_double {
+ LegacyBlockState {
+ id: 125,
+ data: wood_variant,
+ }
+ } else {
+ LegacyBlockState {
+ id: 126,
+ data: wood_variant | if is_top { 8 } else { 0 },
+ }
+ });
+ }
+
+ if let Some(slab_variant) = get_stone_slab_variant(name) {
+ return Some(if is_double {
+ LegacyBlockState {
+ id: 43,
+ data: slab_variant,
+ }
+ } else {
+ LegacyBlockState {
+ id: 44,
+ data: slab_variant | if is_top { 8 } else { 0 },
+ }
+ });
+ }
+
+ Some(if is_double {
+ LegacyBlockState { id: 43, data: 0 }
+ } else {
+ LegacyBlockState {
+ id: 44,
+ data: if is_top { 8 } else { 0 },
+ }
+ })
+}
+
+fn get_wood_slab_variant(name: &str) -> Option {
+ match name {
+ "oak_slab" => Some(0),
+ "spruce_slab" => Some(1),
+ "birch_slab" => Some(2),
+ "jungle_slab" => Some(3),
+ "acacia_slab" => Some(4),
+ "dark_oak_slab" => Some(5),
+ _ => None,
+ }
+}
+
+fn get_stone_slab_variant(name: &str) -> Option {
+ match name {
+ "stone_slab" | "smooth_stone_slab" | "andesite_slab" | "polished_andesite_slab"
+ | "diorite_slab" | "polished_diorite_slab" | "granite_slab" | "polished_granite_slab" => {
+ Some(0)
+ }
+ "sandstone_slab" | "smooth_sandstone_slab" | "cut_sandstone_slab"
+ | "red_sandstone_slab" | "smooth_red_sandstone_slab" | "cut_red_sandstone_slab" => Some(1),
+ "cobblestone_slab" | "mossy_cobblestone_slab" | "cobbled_deepslate_slab"
+ | "deepslate_brick_slab" | "deepslate_tile_slab" => Some(3),
+ "brick_slab" => Some(4),
+ "stone_brick_slab" | "mossy_stone_brick_slab" => Some(5),
+ "nether_brick_slab" | "red_nether_brick_slab" => Some(6),
+ "quartz_slab" | "smooth_quartz_slab" | "purpur_slab" | "prismarine_slab"
+ | "prismarine_brick_slab" | "dark_prismarine_slab" => Some(7),
+ _ => None,
+ }
+}
+
+fn try_get_stairs_id(name: &str) -> Option {
+ match name {
+ "oak_stairs" | "spruce_stairs" | "birch_stairs" | "jungle_stairs" | "acacia_stairs"
+ | "dark_oak_stairs" => Some(53),
+ "cobblestone_stairs" | "stone_stairs" | "mossy_cobblestone_stairs"
+ | "cobbled_deepslate_stairs" | "blackstone_stairs" => Some(67),
+ "brick_stairs" => Some(108),
+ "stone_brick_stairs" | "dark_prismarine_stairs" | "prismarine_stairs"
+ | "prismarine_brick_stairs" | "polished_blackstone_brick_stairs"
+ | "mossy_stone_brick_stairs" | "deepslate_brick_stairs" | "deepslate_tile_stairs" => {
+ Some(109)
+ }
+ "nether_brick_stairs" | "crimson_stairs" | "warped_stairs" => Some(114),
+ "sandstone_stairs" | "red_sandstone_stairs" => Some(128),
+ "quartz_stairs" | "purpur_stairs" => Some(156),
+ "spruce_stairs" => Some(134),
+ "birch_stairs" => Some(135),
+ "jungle_stairs" => Some(136),
+ "acacia_stairs" => Some(163),
+ "dark_oak_stairs" => Some(164),
+ _ => None,
+ }
+}
+
+fn map_stairs_facing(facing: &str) -> u8 {
+ match facing {
+ "east" => 0,
+ "west" => 1,
+ "south" => 2,
+ "north" => 3,
+ _ => 0,
+ }
+}
+
+fn map_ladder_facing(facing: &str) -> u8 {
+ match facing {
+ "north" => 2,
+ "south" => 3,
+ "west" => 4,
+ "east" => 5,
+ _ => 2,
+ }
+}
+
+fn map_button_facing(facing: &str) -> u8 {
+ match facing {
+ "east" => 1,
+ "west" => 2,
+ "south" => 3,
+ "north" => 4,
+ _ => 1,
+ }
+}
+
+fn map_fence_gate_facing(facing: &str) -> u8 {
+ match facing {
+ "south" => 0,
+ "west" => 1,
+ "north" => 2,
+ "east" => 3,
+ _ => 0,
+ }
+}
+
+fn map_vine_faces(properties: Option<&NbtCompound>) -> u8 {
+ let mut data = 0;
+ if get_bool_property(properties, "south") {
+ data |= 1;
+ }
+ if get_bool_property(properties, "west") {
+ data |= 2;
+ }
+ if get_bool_property(properties, "north") {
+ data |= 4;
+ }
+ if get_bool_property(properties, "east") {
+ data |= 8;
+ }
+ data
+}
+
+fn map_lever_data(face: &str, facing: &str) -> u8 {
+ match face {
+ "floor" => {
+ if facing == "east" || facing == "west" {
+ 6
+ } else {
+ 5
+ }
+ }
+ "ceiling" => {
+ if facing == "east" || facing == "west" {
+ 7
+ } else {
+ 0
+ }
+ }
+ _ => map_button_facing(facing),
+ }
+}
+
+fn map_door_facing(facing: &str) -> u8 {
+ match facing {
+ "east" => 0,
+ "south" => 1,
+ "west" => 2,
+ "north" => 3,
+ _ => 0,
+ }
+}
+
+fn map_trapdoor_facing(facing: &str) -> u8 {
+ match facing {
+ "north" => 0,
+ "south" => 1,
+ "west" => 2,
+ "east" => 3,
+ _ => 0,
+ }
+}
+
+fn map_facing_data(facing: &str) -> u8 {
+ match facing {
+ "down" => 0,
+ "up" => 1,
+ "north" => 2,
+ "south" => 3,
+ "west" => 4,
+ "east" => 5,
+ _ => 3,
+ }
+}
+
+fn map_repeater_direction(facing: &str) -> u8 {
+ match facing {
+ "south" => 0,
+ "west" => 1,
+ "north" => 2,
+ "east" => 3,
+ _ => 0,
+ }
+}
+
+fn map_wall_torch_facing(facing: &str) -> u8 {
+ match facing {
+ "east" => 1,
+ "west" => 2,
+ "south" => 3,
+ "north" => 4,
+ _ => 1,
+ }
+}
+
+fn try_map_colored(name: &str, properties: Option<&NbtCompound>) -> Option {
+ let color = get_color_data(&get_property(properties, "color"));
+ match name {
+ "wool" => Some(LegacyBlockState { id: 35, data: color }),
+ "stained_glass" => Some(LegacyBlockState { id: 95, data: color }),
+ "stained_glass_pane" => Some(LegacyBlockState { id: 160, data: color }),
+ "stained_hardened_clay" | "terracotta" => {
+ Some(LegacyBlockState { id: 159, data: color })
+ }
+ "concrete" => Some(LegacyBlockState {
+ id: 172,
+ data: color,
+ }),
+ "concrete_powder" => Some(LegacyBlockState { id: 12, data: color }),
+ "glazed_terracotta" => Some(LegacyBlockState { id: 159, data: color }),
+ _ => None,
+ }
+}
+
+fn try_map_flattened_colored(name: &str) -> Option {
+ let (color_data, suffix) = split_color_prefix(name)?;
+ let data = color_data;
+ match suffix.as_str() {
+ "wool" => Some(LegacyBlockState { id: 35, data }),
+ "stained_glass" | "glass" => Some(LegacyBlockState { id: 95, data }),
+ "stained_glass_pane" | "glass_pane" => Some(LegacyBlockState { id: 160, data }),
+ "stained_hardened_clay" | "terracotta" => Some(LegacyBlockState { id: 159, data }),
+ "concrete" => Some(LegacyBlockState { id: 172, data }),
+ "concrete_powder" => Some(LegacyBlockState { id: 12, data }),
+ "glazed_terracotta" => Some(LegacyBlockState { id: 159, data }),
+ "carpet" => Some(LegacyBlockState { id: 171, data }),
+ _ => None,
+ }
+}
+
+fn split_color_prefix(name: &str) -> Option<(u8, String)> {
+ for color_name in COLOR_NAMES {
+ let prefix = format!("{}_", color_name);
+ if name.starts_with(&prefix) {
+ let suffix = name[prefix.len()..].to_string();
+ return Some((get_color_data(color_name), suffix));
+ }
+ }
+ None
+}
+
+const COLOR_NAMES: &[&str] = &[
+ "white", "orange", "magenta", "light_blue", "yellow", "lime", "pink", "gray", "silver",
+ "light_gray", "cyan", "purple", "blue", "brown", "green", "red", "black",
+];
+
+fn get_color_data(color: &str) -> u8 {
+ match color {
+ "white" => 0,
+ "orange" => 1,
+ "magenta" => 2,
+ "light_blue" => 3,
+ "yellow" => 4,
+ "lime" => 5,
+ "pink" => 6,
+ "gray" => 7,
+ "light_gray" => 8,
+ "cyan" => 9,
+ "purple" => 10,
+ "blue" => 11,
+ "brown" => 12,
+ "green" => 13,
+ "red" => 14,
+ "black" => 15,
+ _ => 0,
+ }
+}
+
+fn try_map_wood(name: &str, properties: Option<&NbtCompound>) -> Option {
+ let wood_type = get_property(properties, "variant");
+ let wood_type = if wood_type.is_empty() {
+ get_prefix_before_underscore(name)
+ } else {
+ wood_type
+ };
+
+ let data_value = match wood_type.as_str() {
+ "spruce" => 1,
+ "birch" => 2,
+ "jungle" => 3,
+ "acacia" => 0,
+ "dark_oak" => 1,
+ _ => 0,
+ };
+
+ if name.ends_with("_planks") || name == "planks" {
+ return Some(LegacyBlockState { id: 5, data: data_value });
+ }
+ if name.ends_with("_sapling") || name == "sapling" {
+ return Some(LegacyBlockState { id: 6, data: data_value });
+ }
+ if name.ends_with("_log") || name == "log" {
+ return Some(LegacyBlockState {
+ id: 17,
+ data: data_value.min(3),
+ });
+ }
+ if name.ends_with("_leaves") || name == "leaves" {
+ return Some(LegacyBlockState {
+ id: 18,
+ data: data_value.min(3),
+ });
+ }
+ if name.ends_with("_stairs") && name.contains("wood") {
+ return Some(LegacyBlockState { id: 53, data: 0 });
+ }
+ if name.ends_with("_door") {
+ return Some(LegacyBlockState { id: 64, data: 0 });
+ }
+ if name.ends_with("_fence") {
+ return Some(LegacyBlockState { id: 85, data: 0 });
+ }
+ if name.ends_with("_fence_gate") {
+ return Some(LegacyBlockState { id: 107, data: 0 });
+ }
+ if name.ends_with("_pressure_plate") && is_wood_family(name) {
+ return Some(LegacyBlockState { id: 72, data: 0 });
+ }
+
+ None
+}
+
+fn try_map_variant(name: &str, properties: Option<&NbtCompound>) -> Option {
+ if let Some(fallback) = try_map_common_fallback(name, properties) {
+ return Some(fallback);
+ }
+
+ if name == "redstone_lamp" {
+ let lit = get_bool_property(properties, "lit");
+ return Some(LegacyBlockState {
+ id: if lit { 124 } else { 123 },
+ data: 0,
+ });
+ }
+
+ let result = match name {
+ "deepslate" | "polished_deepslate" | "tuff" | "calcite" | "dripstone_block" => {
+ Some(LegacyBlockState { id: 1, data: 0 })
+ }
+ "cobbled_deepslate" => Some(LegacyBlockState { id: 4, data: 0 }),
+ "deepslate_bricks" | "deepslate_tiles" => Some(LegacyBlockState { id: 98, data: 0 }),
+ "cracked_deepslate_bricks" | "cracked_deepslate_tiles" => {
+ Some(LegacyBlockState { id: 98, data: 2 })
+ }
+ "chiseled_deepslate" => Some(LegacyBlockState { id: 98, data: 3 }),
+ "deepslate_coal_ore" => Some(LegacyBlockState { id: 16, data: 0 }),
+ "deepslate_iron_ore" | "deepslate_copper_ore" => {
+ Some(LegacyBlockState { id: 15, data: 0 })
+ }
+ "deepslate_gold_ore" => Some(LegacyBlockState { id: 14, data: 0 }),
+ "deepslate_redstone_ore" => Some(LegacyBlockState { id: 73, data: 0 }),
+ "deepslate_lapis_ore" => Some(LegacyBlockState { id: 21, data: 0 }),
+ "deepslate_diamond_ore" => Some(LegacyBlockState { id: 56, data: 0 }),
+ "deepslate_emerald_ore" => Some(LegacyBlockState { id: 129, data: 0 }),
+ "deepslate_tile_stairs" | "deepslate_brick_stairs" | "cobbled_deepslate_stairs" => {
+ Some(LegacyBlockState { id: 67, data: 0 })
+ }
+ "deepslate_tile_slab" | "deepslate_brick_slab" | "cobbled_deepslate_slab" => {
+ Some(LegacyBlockState { id: 44, data: 3 })
+ }
+ "deepslate_tile_wall" | "deepslate_brick_wall" | "cobbled_deepslate_wall" => {
+ Some(LegacyBlockState { id: 139, data: 0 })
+ }
+ "grass_block" => Some(LegacyBlockState { id: 2, data: 0 }),
+ "coarse_dirt" | "podzol" => Some(LegacyBlockState { id: 3, data: 0 }),
+ "grass_path" | "dirt_path" | "moss_carpet" => Some(LegacyBlockState { id: 2, data: 0 }),
+ "cobblestone_wall" => Some(LegacyBlockState { id: 139, data: 0 }),
+ "mossy_cobblestone_wall" => Some(LegacyBlockState { id: 139, data: 1 }),
+ "smooth_stone_slab" => Some(LegacyBlockState { id: 44, data: 0 }),
+ "stone_brick_stairs" => Some(LegacyBlockState { id: 109, data: 0 }),
+ "mossy_stone_bricks" => Some(LegacyBlockState { id: 98, data: 1 }),
+ "cracked_stone_bricks" => Some(LegacyBlockState { id: 98, data: 2 }),
+ "chiseled_stone_bricks" => Some(LegacyBlockState { id: 98, data: 3 }),
+ "red_sand" => Some(LegacyBlockState { id: 12, data: 1 }),
+ "red_sandstone" => Some(LegacyBlockState { id: 24, data: 0 }),
+ "smooth_sandstone" => Some(LegacyBlockState { id: 24, data: 2 }),
+ "chiseled_sandstone" | "cut_sandstone" => Some(LegacyBlockState { id: 24, data: 1 }),
+ "smooth_red_sandstone" | "chiseled_red_sandstone" | "cut_red_sandstone" => {
+ Some(LegacyBlockState { id: 24, data: 0 })
+ }
+ "red_sandstone_stairs" => Some(LegacyBlockState { id: 128, data: 0 }),
+ "red_sandstone_slab" => Some(LegacyBlockState { id: 44, data: 1 }),
+ "prismarine" | "prismarine_bricks" | "dark_prismarine" => {
+ Some(LegacyBlockState { id: 1, data: 0 })
+ }
+ "sea_lantern" => Some(LegacyBlockState { id: 89, data: 0 }),
+ "purpur_block" | "purpur_pillar" => Some(LegacyBlockState { id: 155, data: 0 }),
+ "purpur_stairs" => Some(LegacyBlockState { id: 156, data: 0 }),
+ "purpur_slab" => Some(LegacyBlockState { id: 44, data: 0 }),
+ "end_stone_bricks" => Some(LegacyBlockState { id: 121, data: 0 }),
+ "magma_block" => Some(LegacyBlockState { id: 87, data: 0 }),
+ "nether_wart_block" | "red_nether_bricks" => Some(LegacyBlockState { id: 112, data: 0 }),
+ "bone_block" => Some(LegacyBlockState { id: 1, data: 0 }),
+ "packed_ice" | "blue_ice" => Some(LegacyBlockState { id: 79, data: 0 }),
+ "observer" | "beetroots" | "kelp" | "kelp_plant" | "seagrass" | "tall_seagrass"
+ | "coral_block" | "tube_coral_block" | "brain_coral_block" | "bubble_coral_block"
+ | "fire_coral_block" | "horn_coral_block" | "end_rod" | "chorus_plant"
+ | "chorus_flower" | "end_gateway" | "structure_void" => {
+ Some(LegacyBlockState { id: 0, data: 0 })
+ }
+ "sunflower" | "lilac" | "rose_bush" | "peony" => {
+ if get_property(properties, "half") == "upper" {
+ Some(LegacyBlockState { id: 0, data: 0 })
+ } else {
+ Some(LegacyBlockState { id: 38, data: 0 })
+ }
+ }
+ "tall_grass" => {
+ if get_property(properties, "half") == "upper" {
+ Some(LegacyBlockState { id: 0, data: 0 })
+ } else {
+ let grass_type = get_property(properties, "type");
+ Some(LegacyBlockState {
+ id: 31,
+ data: if grass_type == "fern" { 2 } else { 1 },
+ })
+ }
+ }
+ "large_fern" => {
+ if get_property(properties, "half") == "upper" {
+ Some(LegacyBlockState { id: 0, data: 0 })
+ } else {
+ Some(LegacyBlockState { id: 31, data: 2 })
+ }
+ }
+ "stone" => {
+ let _type = get_property(properties, "type");
+ Some(LegacyBlockState { id: 1, data: 0 })
+ }
+ "sandstone" => {
+ let stype = get_property(properties, "type");
+ Some(LegacyBlockState {
+ id: 24,
+ data: match stype.as_str() {
+ "chiseled" => 1,
+ "smooth" => 2,
+ _ => 0,
+ },
+ })
+ }
+ "stone_bricks" => {
+ let stype = get_property(properties, "type");
+ Some(LegacyBlockState {
+ id: 98,
+ data: match stype.as_str() {
+ "mossy" => 1,
+ "cracked" => 2,
+ "chiseled" => 3,
+ _ => 0,
+ },
+ })
+ }
+ _ => None,
+ };
+
+ result
+}
+
+fn try_map_common_fallback(name: &str, properties: Option<&NbtCompound>) -> Option {
+ if name.ends_with("_button") {
+ return Some(LegacyBlockState {
+ id: if is_wood_family(name) { 143 } else { 77 },
+ data: 0,
+ });
+ }
+ if name.ends_with("_wall_sign") {
+ return Some(LegacyBlockState {
+ id: 68,
+ data: map_wall_sign_facing(&get_property(properties, "facing")),
+ });
+ }
+ if name.ends_with("_sign") {
+ return Some(LegacyBlockState {
+ id: 63,
+ data: (get_int_property(properties, "rotation", 0) & 0x0F) as u8,
+ });
+ }
+ if name.ends_with("_wall") {
+ return Some(LegacyBlockState { id: 139, data: 0 });
+ }
+ if name.ends_with("_bed") {
+ let mut data = map_bed_facing(&get_property(properties, "facing"));
+ if get_property(properties, "part") == "head" {
+ data |= 8;
+ }
+ return Some(LegacyBlockState { id: 26, data });
+ }
+ if name.ends_with("_banner") {
+ return Some(LegacyBlockState { id: 0, data: 0 });
+ }
+
+ match name {
+ "barrier" | "structure_void" => Some(LegacyBlockState { id: 0, data: 0 }),
+ "bamboo" => Some(LegacyBlockState { id: 83, data: 0 }),
+ "cake" => Some(LegacyBlockState { id: 92, data: 0 }),
+ "brewing_stand" => Some(LegacyBlockState { id: 117, data: 0 }),
+ "barrel" => Some(LegacyBlockState { id: 54, data: 0 }),
+ "blast_furnace" => Some(LegacyBlockState { id: 61, data: 0 }),
+ "campfire" => Some(LegacyBlockState { id: 50, data: 0 }),
+ "azure_bluet" | "blue_orchid" => Some(LegacyBlockState { id: 38, data: 0 }),
+ "attached_melon_stem" => Some(LegacyBlockState { id: 105, data: 7 }),
+ "attached_pumpkin_stem" => Some(LegacyBlockState { id: 104, data: 7 }),
+ "andesite" | "diorite" | "granite" | "polished_andesite" | "polished_diorite"
+ | "polished_granite" => Some(LegacyBlockState { id: 1, data: 0 }),
+ "andesite_stairs" | "diorite_stairs" | "granite_stairs" | "polished_andesite_stairs"
+ | "polished_diorite_stairs" | "polished_granite_stairs" => {
+ Some(LegacyBlockState { id: 109, data: 0 })
+ }
+ "bubble_column" => Some(LegacyBlockState { id: 9, data: 0 }),
+ "amethyst_block" | "budding_amethyst" | "amethyst_cluster" => {
+ Some(LegacyBlockState { id: 20, data: 0 })
+ }
+ _ => None,
+ }
+}
+
+fn map_bed_facing(facing: &str) -> u8 {
+ match facing {
+ "south" => 0,
+ "west" => 1,
+ "north" => 2,
+ "east" => 3,
+ _ => 0,
+ }
+}
+
+fn map_wall_sign_facing(facing: &str) -> u8 {
+ match facing {
+ "north" => 2,
+ "south" => 3,
+ "west" => 4,
+ "east" => 5,
+ _ => 2,
+ }
+}
+
+fn get_prefix_before_underscore(value: &str) -> String {
+ let index = value.find('_');
+ match index {
+ Some(i) if i > 0 => value[..i].to_string(),
+ _ => value.to_string(),
+ }
+}
+
+fn is_wood_family(name: &str) -> bool {
+ name.contains("oak")
+ || name.contains("spruce")
+ || name.contains("birch")
+ || name.contains("jungle")
+ || name.contains("acacia")
+ || name.contains("dark_oak")
+ || name.contains("mangrove")
+ || name.contains("cherry")
+ || name.contains("bamboo")
+ || name.contains("crimson")
+ || name.contains("warped")
+}
+
+pub fn is_valid_lce_tile_id(id: u8) -> bool {
+ id <= 160 || (id >= 170 && id <= 173)
+}
+
+pub fn remap_blocks(blocks: &mut [u8], _data: &mut [u8]) {
+ for id in blocks.iter_mut() {
+ let mut bid = *id;
+ if bid == 137 {
+ bid = 1;
+ }
+ if bid >= 165 {
+ if let Some(&replacement) = BLOCK_REMAP_TABLE.get(&bid) {
+ bid = replacement;
+ }
+ }
+ if !is_valid_lce_tile_id(bid) {
+ bid = 0;
+ }
+ *id = bid;
+ }
+}
+
+static MODERN_DIRECT_MAP: once_cell::sync::Lazy> =
+ once_cell::sync::Lazy::new(|| {
+ let mut m = HashMap::new();
+ m.insert("air", LegacyBlockState { id: 0, data: 0 });
+ m.insert("cave_air", LegacyBlockState { id: 0, data: 0 });
+ m.insert("void_air", LegacyBlockState { id: 0, data: 0 });
+ m.insert("stone", LegacyBlockState { id: 1, data: 0 });
+ m.insert("grass", LegacyBlockState { id: 31, data: 1 });
+ m.insert("short_grass", LegacyBlockState { id: 31, data: 1 });
+ m.insert("short_dry_grass", LegacyBlockState { id: 31, data: 1 });
+ m.insert("dirt", LegacyBlockState { id: 3, data: 0 });
+ m.insert("cobblestone", LegacyBlockState { id: 4, data: 0 });
+ m.insert("bedrock", LegacyBlockState { id: 7, data: 0 });
+ m.insert("water", LegacyBlockState { id: 8, data: 0 });
+ m.insert("lava", LegacyBlockState { id: 10, data: 0 });
+ m.insert("sand", LegacyBlockState { id: 12, data: 0 });
+ m.insert("gravel", LegacyBlockState { id: 13, data: 0 });
+ m.insert("gold_ore", LegacyBlockState { id: 14, data: 0 });
+ m.insert("iron_ore", LegacyBlockState { id: 15, data: 0 });
+ m.insert("coal_ore", LegacyBlockState { id: 16, data: 0 });
+ m.insert("oak_log", LegacyBlockState { id: 17, data: 0 });
+ m.insert("oak_leaves", LegacyBlockState { id: 18, data: 0 });
+ m.insert("glass", LegacyBlockState { id: 20, data: 0 });
+ m.insert("lapis_ore", LegacyBlockState { id: 21, data: 0 });
+ m.insert("lapis_block", LegacyBlockState { id: 22, data: 0 });
+ m.insert("dispenser", LegacyBlockState { id: 23, data: 0 });
+ m.insert("sandstone", LegacyBlockState { id: 24, data: 0 });
+ m.insert("note_block", LegacyBlockState { id: 25, data: 0 });
+ m.insert("powered_rail", LegacyBlockState { id: 27, data: 0 });
+ m.insert("detector_rail", LegacyBlockState { id: 28, data: 0 });
+ m.insert("sticky_piston", LegacyBlockState { id: 29, data: 0 });
+ m.insert("cobweb", LegacyBlockState { id: 30, data: 0 });
+ m.insert("dead_bush", LegacyBlockState { id: 32, data: 0 });
+ m.insert("piston", LegacyBlockState { id: 33, data: 0 });
+ m.insert("wool", LegacyBlockState { id: 35, data: 0 });
+ m.insert("dandelion", LegacyBlockState { id: 37, data: 0 });
+ m.insert("poppy", LegacyBlockState { id: 38, data: 0 });
+ m.insert("brown_mushroom", LegacyBlockState { id: 39, data: 0 });
+ m.insert("red_mushroom", LegacyBlockState { id: 40, data: 0 });
+ m.insert("gold_block", LegacyBlockState { id: 41, data: 0 });
+ m.insert("iron_block", LegacyBlockState { id: 42, data: 0 });
+ m.insert("stone_slab", LegacyBlockState { id: 44, data: 0 });
+ m.insert("bricks", LegacyBlockState { id: 45, data: 0 });
+ m.insert("tnt", LegacyBlockState { id: 46, data: 0 });
+ m.insert("bookshelf", LegacyBlockState { id: 47, data: 0 });
+ m.insert("mossy_cobblestone", LegacyBlockState { id: 48, data: 0 });
+ m.insert("obsidian", LegacyBlockState { id: 49, data: 0 });
+ m.insert("torch", LegacyBlockState { id: 50, data: 0 });
+ m.insert("fire", LegacyBlockState { id: 51, data: 0 });
+ m.insert("mob_spawner", LegacyBlockState { id: 52, data: 0 });
+ m.insert("oak_stairs", LegacyBlockState { id: 53, data: 0 });
+ m.insert("chest", LegacyBlockState { id: 54, data: 0 });
+ m.insert("diamond_ore", LegacyBlockState { id: 56, data: 0 });
+ m.insert("diamond_block", LegacyBlockState { id: 57, data: 0 });
+ m.insert("crafting_table", LegacyBlockState { id: 58, data: 0 });
+ m.insert("farmland", LegacyBlockState { id: 60, data: 0 });
+ m.insert("furnace", LegacyBlockState { id: 61, data: 0 });
+ m.insert("ladder", LegacyBlockState { id: 65, data: 0 });
+ m.insert("rail", LegacyBlockState { id: 66, data: 0 });
+ m.insert("lever", LegacyBlockState { id: 69, data: 0 });
+ m.insert("stone_pressure_plate", LegacyBlockState { id: 70, data: 0 });
+ m.insert("oak_pressure_plate", LegacyBlockState { id: 72, data: 0 });
+ m.insert("redstone_ore", LegacyBlockState { id: 73, data: 0 });
+ m.insert("redstone_torch", LegacyBlockState { id: 76, data: 0 });
+ m.insert("stone_button", LegacyBlockState { id: 77, data: 0 });
+ m.insert("snow", LegacyBlockState { id: 78, data: 0 });
+ m.insert("ice", LegacyBlockState { id: 79, data: 0 });
+ m.insert("snow_block", LegacyBlockState { id: 80, data: 0 });
+ m.insert("cactus", LegacyBlockState { id: 81, data: 0 });
+ m.insert("clay", LegacyBlockState { id: 82, data: 0 });
+ m.insert("jukebox", LegacyBlockState { id: 84, data: 0 });
+ m.insert("oak_fence", LegacyBlockState { id: 85, data: 0 });
+ m.insert("netherrack", LegacyBlockState { id: 87, data: 0 });
+ m.insert("soul_sand", LegacyBlockState { id: 88, data: 0 });
+ m.insert("glowstone", LegacyBlockState { id: 89, data: 0 });
+ m.insert("jack_o_lantern", LegacyBlockState { id: 91, data: 0 });
+ m.insert("stone_bricks", LegacyBlockState { id: 98, data: 0 });
+ m.insert("brown_mushroom_block", LegacyBlockState { id: 99, data: 0 });
+ m.insert("red_mushroom_block", LegacyBlockState { id: 100, data: 0 });
+ m.insert("iron_bars", LegacyBlockState { id: 101, data: 0 });
+ m.insert("glass_pane", LegacyBlockState { id: 102, data: 0 });
+ m.insert("melon", LegacyBlockState { id: 103, data: 0 });
+ m.insert("vine", LegacyBlockState { id: 106, data: 0 });
+ m.insert("oak_fence_gate", LegacyBlockState { id: 107, data: 0 });
+ m.insert("brick_stairs", LegacyBlockState { id: 108, data: 0 });
+ m.insert("stone_brick_stairs", LegacyBlockState { id: 109, data: 0 });
+ m.insert("mycelium", LegacyBlockState { id: 110, data: 0 });
+ m.insert("lily_pad", LegacyBlockState { id: 111, data: 0 });
+ m.insert("nether_bricks", LegacyBlockState { id: 112, data: 0 });
+ m.insert("nether_brick_fence", LegacyBlockState { id: 113, data: 0 });
+ m.insert("nether_brick_stairs", LegacyBlockState { id: 114, data: 0 });
+ m.insert("enchanting_table", LegacyBlockState { id: 116, data: 0 });
+ m.insert("end_stone", LegacyBlockState { id: 121, data: 0 });
+ m.insert("sandstone_stairs", LegacyBlockState { id: 128, data: 0 });
+ m.insert("emerald_ore", LegacyBlockState { id: 129, data: 0 });
+ m.insert("ender_chest", LegacyBlockState { id: 130, data: 0 });
+ m.insert("tripwire_hook", LegacyBlockState { id: 131, data: 0 });
+ m.insert("emerald_block", LegacyBlockState { id: 133, data: 0 });
+ m.insert("spruce_stairs", LegacyBlockState { id: 134, data: 0 });
+ m.insert("birch_stairs", LegacyBlockState { id: 135, data: 0 });
+ m.insert("jungle_stairs", LegacyBlockState { id: 136, data: 0 });
+ m.insert("command_block", LegacyBlockState { id: 1, data: 0 });
+ m.insert("beacon", LegacyBlockState { id: 138, data: 0 });
+ m.insert("cobblestone_wall", LegacyBlockState { id: 139, data: 0 });
+ m.insert("flower_pot", LegacyBlockState { id: 140, data: 0 });
+ m.insert("carrots", LegacyBlockState { id: 141, data: 0 });
+ m.insert("potatoes", LegacyBlockState { id: 142, data: 0 });
+ m.insert("oak_button", LegacyBlockState { id: 143, data: 0 });
+ m.insert("anvil", LegacyBlockState { id: 145, data: 0 });
+ m.insert("trapped_chest", LegacyBlockState { id: 146, data: 0 });
+ m.insert("light_weighted_pressure_plate", LegacyBlockState { id: 147, data: 0 });
+ m.insert("heavy_weighted_pressure_plate", LegacyBlockState { id: 148, data: 0 });
+ m.insert("daylight_detector", LegacyBlockState { id: 151, data: 0 });
+ m.insert("redstone_block", LegacyBlockState { id: 152, data: 0 });
+ m.insert("quartz_ore", LegacyBlockState { id: 153, data: 0 });
+ m.insert("hopper", LegacyBlockState { id: 154, data: 0 });
+ m.insert("quartz_block", LegacyBlockState { id: 155, data: 0 });
+ m.insert("quartz_stairs", LegacyBlockState { id: 156, data: 0 });
+ m.insert("activator_rail", LegacyBlockState { id: 157, data: 0 });
+ m.insert("dropper", LegacyBlockState { id: 158, data: 0 });
+ m.insert("stained_hardened_clay", LegacyBlockState { id: 159, data: 0 });
+ m.insert("stained_glass", LegacyBlockState { id: 95, data: 0 });
+ m.insert("stained_glass_pane", LegacyBlockState { id: 160, data: 0 });
+ m.insert("leaves2", LegacyBlockState { id: 161, data: 0 });
+ m.insert("log2", LegacyBlockState { id: 162, data: 0 });
+ m.insert("acacia_stairs", LegacyBlockState { id: 163, data: 0 });
+ m.insert("dark_oak_stairs", LegacyBlockState { id: 164, data: 0 });
+ m.insert("fern", LegacyBlockState { id: 31, data: 2 });
+ m.insert("sugar_cane", LegacyBlockState { id: 83, data: 0 });
+ m.insert("pumpkin", LegacyBlockState { id: 86, data: 0 });
+ m.insert("carved_pumpkin", LegacyBlockState { id: 86, data: 0 });
+ m.insert("nether_quartz_ore", LegacyBlockState { id: 153, data: 0 });
+ m.insert("dragon_egg", LegacyBlockState { id: 122, data: 0 });
+ m.insert("spawner", LegacyBlockState { id: 52, data: 0 });
+ m.insert("tripwire", LegacyBlockState { id: 132, data: 0 });
+ m.insert("sponge", LegacyBlockState { id: 19, data: 0 });
+ m.insert("wet_sponge", LegacyBlockState { id: 19, data: 0 });
+ m.insert("cauldron", LegacyBlockState { id: 118, data: 0 });
+ m.insert("water_cauldron", LegacyBlockState { id: 118, data: 3 });
+ m.insert("coal_block", LegacyBlockState { id: 173, data: 0 });
+ m.insert("mushroom_stem", LegacyBlockState { id: 99, data: 10 });
+ m.insert("smooth_stone", LegacyBlockState { id: 1, data: 0 });
+ m.insert("infested_stone", LegacyBlockState { id: 1, data: 0 });
+ m.insert("infested_stone_bricks", LegacyBlockState { id: 98, data: 0 });
+ m.insert("infested_deepslate", LegacyBlockState { id: 1, data: 0 });
+ m.insert(
+ "polished_blackstone_bricks",
+ LegacyBlockState { id: 98, data: 0 },
+ );
+ m.insert(
+ "cracked_polished_blackstone_bricks",
+ LegacyBlockState { id: 98, data: 2 },
+ );
+ m.insert("blackstone", LegacyBlockState { id: 4, data: 0 });
+ m.insert("quartz_bricks", LegacyBlockState { id: 155, data: 0 });
+ m.insert(
+ "chiseled_quartz_block",
+ LegacyBlockState { id: 155, data: 1 },
+ );
+ m.insert(
+ "chiseled_nether_bricks",
+ LegacyBlockState { id: 112, data: 0 },
+ );
+ m.insert("smooth_basalt", LegacyBlockState { id: 1, data: 0 });
+ m.insert("pointed_dripstone", LegacyBlockState { id: 1, data: 0 });
+ m.insert("lodestone", LegacyBlockState { id: 1, data: 0 });
+ m.insert("structure_block", LegacyBlockState { id: 1, data: 0 });
+ m.insert("stonecutter", LegacyBlockState { id: 4, data: 0 });
+ m.insert("rooted_dirt", LegacyBlockState { id: 3, data: 0 });
+ m.insert("moss_block", LegacyBlockState { id: 48, data: 0 });
+ m.insert(
+ "dried_kelp_block",
+ LegacyBlockState { id: 170, data: 0 },
+ );
+ m.insert("crimson_stem", LegacyBlockState { id: 17, data: 0 });
+ m.insert("warped_stem", LegacyBlockState { id: 17, data: 0 });
+ m.insert(
+ "stripped_crimson_stem",
+ LegacyBlockState { id: 17, data: 0 },
+ );
+ m.insert(
+ "stripped_warped_stem",
+ LegacyBlockState { id: 17, data: 0 },
+ );
+ m.insert("azalea", LegacyBlockState { id: 18, data: 0 });
+ m.insert("flowering_azalea", LegacyBlockState { id: 18, data: 0 });
+ m.insert(
+ "big_dripleaf",
+ LegacyBlockState { id: 111, data: 0 },
+ );
+ m.insert(
+ "big_dripleaf_stem",
+ LegacyBlockState { id: 17, data: 0 },
+ );
+ m.insert("small_dripleaf", LegacyBlockState { id: 0, data: 0 });
+ m.insert("hanging_roots", LegacyBlockState { id: 0, data: 0 });
+ m.insert("spore_blossom", LegacyBlockState { id: 38, data: 0 });
+ m.insert(
+ "twisting_vines",
+ LegacyBlockState { id: 106, data: 0 },
+ );
+ m.insert(
+ "twisting_vines_plant",
+ LegacyBlockState { id: 106, data: 0 },
+ );
+ m.insert("cave_vines", LegacyBlockState { id: 0, data: 0 });
+ m.insert(
+ "cave_vines_plant",
+ LegacyBlockState { id: 0, data: 0 },
+ );
+ m.insert("glow_lichen", LegacyBlockState { id: 0, data: 0 });
+ m.insert("cornflower", LegacyBlockState { id: 38, data: 0 });
+ m.insert(
+ "lily_of_the_valley",
+ LegacyBlockState { id: 38, data: 0 },
+ );
+ m.insert("oxeye_daisy", LegacyBlockState { id: 38, data: 0 });
+ m.insert("pink_tulip", LegacyBlockState { id: 38, data: 0 });
+ m.insert("brain_coral", LegacyBlockState { id: 38, data: 0 });
+ m.insert("bubble_coral", LegacyBlockState { id: 38, data: 0 });
+ m.insert("fire_coral", LegacyBlockState { id: 38, data: 0 });
+ m.insert("horn_coral", LegacyBlockState { id: 38, data: 0 });
+ m.insert("tube_coral", LegacyBlockState { id: 38, data: 0 });
+ m.insert(
+ "horn_coral_fan",
+ LegacyBlockState { id: 38, data: 0 },
+ );
+ m.insert(
+ "tube_coral_fan",
+ LegacyBlockState { id: 38, data: 0 },
+ );
+ m.insert("smoker", LegacyBlockState { id: 61, data: 0 });
+ m.insert(
+ "grindstone",
+ LegacyBlockState { id: 145, data: 0 },
+ );
+ m.insert("bell", LegacyBlockState { id: 145, data: 0 });
+ m.insert("loom", LegacyBlockState { id: 58, data: 0 });
+ m.insert(
+ "smithing_table",
+ LegacyBlockState { id: 58, data: 0 },
+ );
+ m.insert(
+ "target",
+ LegacyBlockState { id: 70, data: 0 },
+ );
+ m.insert("chain", LegacyBlockState { id: 101, data: 0 });
+ m.insert(
+ "lightning_rod",
+ LegacyBlockState { id: 101, data: 0 },
+ );
+ m.insert("lantern", LegacyBlockState { id: 50, data: 0 });
+ m.insert(
+ "shroomlight",
+ LegacyBlockState { id: 89, data: 0 },
+ );
+ m.insert(
+ "soul_campfire",
+ LegacyBlockState { id: 51, data: 0 },
+ );
+ m.insert(
+ "bee_nest",
+ LegacyBlockState { id: 54, data: 0 },
+ );
+ m.insert(
+ "wither_skeleton_skull",
+ LegacyBlockState { id: 144, data: 0 },
+ );
+ m.insert(
+ "dragon_wall_head",
+ LegacyBlockState { id: 144, data: 0 },
+ );
+ m.insert(
+ "nether_portal",
+ LegacyBlockState { id: 90, data: 0 },
+ );
+ m.insert(
+ "white_candle",
+ LegacyBlockState { id: 50, data: 0 },
+ );
+ m.insert(
+ "orange_candle",
+ LegacyBlockState { id: 50, data: 0 },
+ );
+ m.insert(
+ "gray_candle",
+ LegacyBlockState { id: 50, data: 0 },
+ );
+ m.insert(
+ "cyan_candle",
+ LegacyBlockState { id: 50, data: 0 },
+ );
+ m.insert(
+ "lime_candle",
+ LegacyBlockState { id: 50, data: 0 },
+ );
+ m.insert(
+ "potted_blue_orchid",
+ LegacyBlockState { id: 140, data: 0 },
+ );
+ m.insert(
+ "potted_brown_mushroom",
+ LegacyBlockState { id: 140, data: 0 },
+ );
+ m.insert(
+ "potted_cactus",
+ LegacyBlockState { id: 140, data: 0 },
+ );
+ m.insert(
+ "potted_dandelion",
+ LegacyBlockState { id: 140, data: 0 },
+ );
+ m.insert(
+ "potted_oxeye_daisy",
+ LegacyBlockState { id: 140, data: 0 },
+ );
+ m.insert(
+ "potted_poppy",
+ LegacyBlockState { id: 140, data: 0 },
+ );
+ m.insert(
+ "potted_red_mushroom",
+ LegacyBlockState { id: 140, data: 0 },
+ );
+ m.insert(
+ "powder_snow",
+ LegacyBlockState { id: 80, data: 0 },
+ );
+ m.insert(
+ "copper_ore",
+ LegacyBlockState { id: 15, data: 0 },
+ );
+ m.insert(
+ "oxidized_copper",
+ LegacyBlockState { id: 15, data: 0 },
+ );
+ m.insert(
+ "weathered_copper",
+ LegacyBlockState { id: 15, data: 0 },
+ );
+ m.insert(
+ "raw_copper_block",
+ LegacyBlockState { id: 42, data: 0 },
+ );
+ m.insert(
+ "raw_iron_block",
+ LegacyBlockState { id: 42, data: 0 },
+ );
+ m.insert(
+ "small_amethyst_bud",
+ LegacyBlockState { id: 20, data: 0 },
+ );
+ m.insert(
+ "medium_amethyst_bud",
+ LegacyBlockState { id: 20, data: 0 },
+ );
+ m.insert(
+ "large_amethyst_bud",
+ LegacyBlockState { id: 20, data: 0 },
+ );
+ m.insert(
+ "slime_block",
+ LegacyBlockState { id: 0, data: 0 },
+ );
+ m.insert("light", LegacyBlockState { id: 0, data: 0 });
+ m.insert(
+ "scaffolding",
+ LegacyBlockState { id: 0, data: 0 },
+ );
+ m.insert(
+ "powder_snow_cauldron",
+ LegacyBlockState { id: 118, data: 0 },
+ );
+ m.insert(
+ "lava_cauldron",
+ LegacyBlockState { id: 118, data: 0 },
+ );
+ m
+ });
+
+static BLOCK_REMAP_TABLE: once_cell::sync::Lazy> =
+ once_cell::sync::Lazy::new(|| {
+ let mut m = HashMap::new();
+ m.insert(174, 79);
+ m.insert(175, 0);
+ m.insert(165, 0);
+ m.insert(166, 0);
+ m.insert(167, 96);
+ m.insert(168, 1);
+ m.insert(169, 89);
+ m.insert(176, 0);
+ m.insert(177, 0);
+ m.insert(178, 151);
+ m.insert(179, 24);
+ m.insert(180, 128);
+ m.insert(181, 43);
+ m.insert(182, 44);
+ m.insert(183, 107);
+ m.insert(184, 107);
+ m.insert(185, 107);
+ m.insert(186, 107);
+ m.insert(187, 107);
+ m.insert(188, 85);
+ m.insert(189, 85);
+ m.insert(190, 85);
+ m.insert(191, 85);
+ m.insert(192, 85);
+ m.insert(193, 64);
+ m.insert(194, 64);
+ m.insert(195, 64);
+ m.insert(196, 64);
+ m.insert(197, 64);
+ m.insert(198, 0);
+ m.insert(199, 0);
+ m.insert(200, 0);
+ m.insert(201, 155);
+ m.insert(202, 155);
+ m.insert(203, 156);
+ m.insert(204, 43);
+ m.insert(205, 44);
+ m.insert(206, 121);
+ m.insert(207, 0);
+ m.insert(208, 2);
+ m.insert(209, 0);
+ m.insert(210, 1);
+ m.insert(211, 1);
+ m.insert(212, 79);
+ m.insert(213, 87);
+ m.insert(214, 112);
+ m.insert(215, 112);
+ m.insert(216, 1);
+ m.insert(217, 0);
+ m.insert(218, 0);
+ for i in 219..=234 {
+ m.insert(i, 0);
+ }
+ for i in 235..=250 {
+ m.insert(i, 159);
+ }
+ m.insert(251, 172);
+ m.insert(252, 12);
+ m
+ });
diff --git a/src-tauri/src/commands/java2lce/chunk.rs b/src-tauri/src/commands/java2lce/chunk.rs
new file mode 100644
index 0000000..e279b98
--- /dev/null
+++ b/src-tauri/src/commands/java2lce/chunk.rs
@@ -0,0 +1,1690 @@
+use std::collections::HashMap;
+use super::block_mapping::{self, LegacyBlockState};
+use super::nbt;
+use super::payload;
+const CHUNK_BLOCKS: usize = 65536;
+const CHUNK_NIBBLES: usize = 32768;
+const HEIGHTMAP_SIZE: usize = 256;
+const BIOMES_SIZE: usize = 256;
+const STAIR_UPSIDE_DOWN_BIT: u8 = 4;
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum JavaChunkFormat {
+ Unknown,
+ LegacyBlockArray,
+ LegacyAnvil,
+ ModernPalette,
+ ModernExtendedHeight,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct JavaChunkFormatInfo {
+ pub format: JavaChunkFormat,
+ pub has_level_wrapper: bool,
+ pub data_version: i32,
+ pub min_section_y: Option,
+ pub max_section_y: Option,
+ pub has_modern_entity_tags: bool,
+}
+
+impl JavaChunkFormatInfo {
+ pub fn is_section_based(&self) -> bool {
+ matches!(
+ self.format,
+ JavaChunkFormat::LegacyAnvil
+ | JavaChunkFormat::ModernPalette
+ | JavaChunkFormat::ModernExtendedHeight
+ )
+ }
+
+ pub fn uses_palette_sections(&self) -> bool {
+ matches!(
+ self.format,
+ JavaChunkFormat::ModernPalette | JavaChunkFormat::ModernExtendedHeight
+ )
+ }
+
+ pub fn uses_modern_content_schema(&self) -> bool {
+ self.uses_palette_sections() || self.has_modern_entity_tags
+ }
+
+ pub fn requires_section_shift(&self) -> bool {
+ self.format == JavaChunkFormat::ModernExtendedHeight
+ }
+}
+
+pub fn inspect_chunk(root_tag: &nbt::NbtCompound) -> JavaChunkFormatInfo {
+ let has_level_wrapper = root_tag.get("Level").is_some();
+ let source_level = root_tag
+ .compound("Level")
+ .unwrap_or(root_tag);
+
+ let data_version = root_tag
+ .int("DataVersion")
+ .or_else(|| {
+ root_tag
+ .compound("Level")
+ .and_then(|l| l.int("DataVersion"))
+ })
+ .unwrap_or(0);
+
+ let has_modern_entity_tags = source_level.contains("block_entities") || source_level.contains("entities");
+ let has_legacy_block_arrays = source_level.contains("Blocks");
+ let sections = source_level
+ .list("Sections")
+ .or_else(|| source_level.list("sections"));
+
+ let mut has_sections = false;
+ let mut has_palette_sections = false;
+ let mut min_section_y: Option = None;
+ let mut max_section_y: Option = None;
+ if let Some(sects) = sections {
+ has_sections = !sects.is_empty();
+ for tag in sects {
+ if let nbt::NbtValue::Compound(section) = tag {
+ if let Some(sy) = read_section_y(section) {
+ min_section_y = Some(min_section_y.map_or(sy, |v| v.min(sy)));
+ max_section_y = Some(max_section_y.map_or(sy, |v| v.max(sy)));
+ }
+ if section_uses_palette_storage(section) {
+ has_palette_sections = true;
+ }
+ }
+ }
+ }
+
+ let format = if has_palette_sections {
+ let has_extended = min_section_y.map_or(false, |v| v < 0)
+ || max_section_y.map_or(false, |v| v > 15);
+ if has_extended {
+ JavaChunkFormat::ModernExtendedHeight
+ } else {
+ JavaChunkFormat::ModernPalette
+ }
+ } else if has_sections {
+ JavaChunkFormat::LegacyAnvil
+ } else if has_legacy_block_arrays {
+ JavaChunkFormat::LegacyBlockArray
+ } else {
+ JavaChunkFormat::Unknown
+ };
+
+ JavaChunkFormatInfo {
+ format,
+ has_level_wrapper,
+ data_version,
+ min_section_y,
+ max_section_y,
+ has_modern_entity_tags,
+ }
+}
+
+pub fn read_section_y(section: &nbt::NbtCompound) -> Option {
+ if let Some(b) = section.byte("Y") {
+ return Some(b as i8 as i32);
+ }
+ if let Some(i) = section.int("Y") {
+ return Some(i);
+ }
+ if let Some(s) = section.short("Y") {
+ return Some(s as i32);
+ }
+ None
+}
+
+fn section_uses_palette_storage(section: &nbt::NbtCompound) -> bool {
+ section.contains("block_states")
+ || section.contains("Palette")
+ || section.contains("BlockStates")
+}
+
+fn get_byte_array_or_default(compound: &nbt::NbtCompound, name: &str, default_size: usize) -> Vec {
+ match compound.byte_array(name) {
+ Some(bytes) => {
+ let mut v = bytes.to_vec();
+ if v.len() < default_size {
+ v.resize(default_size, 0);
+ } else {
+ v.truncate(default_size);
+ }
+ v
+ }
+ None => vec![0u8; default_size],
+ }
+}
+
+struct DecodedSection {
+ section_y: i32,
+ blocks: Vec,
+ data: Vec,
+ sky_light: Option>,
+ block_light: Option>,
+ non_air_count: u32,
+}
+
+fn count_non_air(blocks: &[u8]) -> u32 {
+ blocks.iter().filter(|&&b| b != 0).count() as u32
+}
+
+fn try_decode_section_blocks(
+ section: &nbt::NbtCompound,
+) -> Option<(Vec, Vec)> {
+ if let Some(old_blocks) = section.byte_array("Blocks") {
+ if old_blocks.len() >= 4096 {
+ let blocks = old_blocks[..4096].to_vec();
+ let data = section
+ .byte_array("Data")
+ .map(|d| d.to_vec())
+ .unwrap_or_else(|| vec![0u8; CHUNK_NIBBLES]);
+ return Some((blocks, data));
+ }
+ }
+ try_decode_palette_section(section)
+}
+
+fn try_decode_palette_section(section: &nbt::NbtCompound) -> Option<(Vec, Vec)> {
+ let mut blocks = vec![0u8; 4096];
+ let mut data = vec![0u8; 2048];
+ let block_states_container = section.compound("block_states");
+ let palette = section
+ .list("Palette")
+ .or_else(|| {
+ block_states_container
+ .and_then(|bs| bs.list("palette"))
+ });
+
+ let palette = palette?;
+ if palette.is_empty() {
+ return None;
+ }
+
+ if palette.len() == 1 {
+ if let nbt::NbtValue::Compound(entry) = &palette[0] {
+ let lb = block_mapping::map_modern_block_state(entry);
+ blocks.fill(lb.id);
+ if lb.data != 0 {
+ for i in 0..4096 {
+ nbt::set_nibble(&mut data, i, lb.data);
+ }
+ }
+ return Some((blocks, data));
+ }
+ return None;
+ }
+
+ let block_states_tag = section
+ .long_array("BlockStates")
+ .or_else(|| {
+ block_states_container
+ .and_then(|bs| bs.long_array("data"))
+ })?;
+
+ if block_states_tag.is_empty() {
+ return None;
+ }
+
+ let bits_per_block = get_bits_required(palette.len() - 1).max(4);
+ let values_per_long = (64 / bits_per_block).max(1);
+ let expected_long_count = (4096 + values_per_long - 1) / values_per_long;
+ let use_padded = block_states_tag.len() == expected_long_count;
+ for i in 0..4096 {
+ let palette_index = if use_padded {
+ read_padded_block_state(block_states_tag, bits_per_block, i)
+ } else {
+ read_compact_block_state(block_states_tag, bits_per_block, i)
+ };
+
+ if (palette_index as usize) >= palette.len() {
+ continue;
+ }
+
+ if let nbt::NbtValue::Compound(entry) = &palette[palette_index] {
+ let legacy = block_mapping::map_modern_block_state(entry);
+ blocks[i] = legacy.id;
+ if legacy.data != 0 {
+ nbt::set_nibble(&mut data, i, legacy.data);
+ }
+ }
+ }
+
+ Some((blocks, data))
+}
+
+fn get_bits_required(value: usize) -> usize {
+ let mut bits = 0;
+ let mut v = value;
+ while v > 0 {
+ bits += 1;
+ v >>= 1;
+ }
+ bits.max(1)
+}
+
+fn read_padded_block_state(block_states: &[i64], bits_per_block: usize, index: usize) -> usize {
+ let values_per_long = (64 / bits_per_block).max(1);
+ let long_index = index / values_per_long;
+ let bit_offset = (index % values_per_long) * bits_per_block;
+ let mask = (1u64 << bits_per_block) - 1;
+ ((block_states[long_index] as u64 >> bit_offset) & mask) as usize
+}
+
+fn read_compact_block_state(block_states: &[i64], bits_per_block: usize, index: usize) -> usize {
+ let start_bit = index * bits_per_block;
+ let long_index = start_bit >> 6;
+ let bit_offset = start_bit & 63;
+ let mask = (1u64 << bits_per_block) - 1;
+ let mut value = block_states[long_index] as u64 >> bit_offset;
+ let bits_read = 64 - bit_offset;
+ if bits_read < bits_per_block && long_index + 1 < block_states.len() {
+ value |= (block_states[long_index + 1] as u64) << bits_read;
+ }
+ (value & mask) as usize
+}
+
+fn flatten_anvil_sections(
+ level: &nbt::NbtCompound,
+ format_info: &JavaChunkFormatInfo,
+ global_section_shift: &mut Option,
+) -> (Vec, Vec, Vec, Vec) {
+ let mut blocks = vec![0u8; CHUNK_BLOCKS];
+ let mut data = vec![0u8; CHUNK_NIBBLES];
+ let mut sky_light = vec![0xFFu8; CHUNK_NIBBLES];
+ let mut block_light = vec![0u8; CHUNK_NIBBLES];
+ let sections = level
+ .list("Sections")
+ .or_else(|| level.list("sections"));
+
+ let sections = match sections {
+ Some(s) => s,
+ None => return (blocks, data, sky_light, block_light),
+ };
+
+ let mut decoded_sections: Vec = Vec::new();
+ for tag in sections {
+ if let nbt::NbtValue::Compound(section) = tag {
+ let section_y = match read_section_y(section) {
+ Some(y) => y,
+ None => continue,
+ };
+
+ let (s_blocks, s_data) = match try_decode_section_blocks(section) {
+ Some(v) => v,
+ None => continue,
+ };
+
+ let s_sky = section
+ .byte_array("SkyLight")
+ .or_else(|| section.byte_array("sky_light"))
+ .map(|b| b.to_vec());
+ let s_block = section
+ .byte_array("BlockLight")
+ .or_else(|| section.byte_array("block_light"))
+ .map(|b| b.to_vec());
+
+ let non_air = count_non_air(&s_blocks);
+ decoded_sections.push(DecodedSection {
+ section_y,
+ blocks: s_blocks,
+ data: s_data,
+ sky_light: s_sky,
+ block_light: s_block,
+ non_air_count: non_air,
+ });
+ }
+ }
+
+ if decoded_sections.is_empty() {
+ return (blocks, data, sky_light, block_light);
+ }
+
+ let mut section_shift = 0;
+ if format_info.requires_section_shift() {
+ if global_section_shift.is_none() {
+ let anchor = decoded_sections
+ .iter()
+ .max_by_key(|s| (s.non_air_count, -(s.section_y - 4).abs()))
+ .map(|s| s.section_y)
+ .unwrap_or(4);
+ *global_section_shift = Some(anchor - 4);
+ }
+ section_shift = global_section_shift.unwrap_or(0);
+ }
+
+ for section in &decoded_sections {
+ let remapped_y = section.section_y - section_shift;
+ if remapped_y < 0 || remapped_y > 15 {
+ continue;
+ }
+
+ let base_y = remapped_y * 16;
+ for i in 0..4096 {
+ let x = i & 0x0F;
+ let z = (i >> 4) & 0x0F;
+ let y = (i >> 8) & 0x0F;
+ let global_y = base_y + y as i32;
+ if global_y < 0 || global_y >= 256 {
+ continue;
+ }
+ let flat_index = ((x * 16) + z) * 256 + global_y as usize;
+ if flat_index < CHUNK_BLOCKS {
+ blocks[flat_index] = section.blocks[i];
+ if section.data.len() > i {
+ nbt::set_nibble(&mut data, flat_index, nbt::get_nibble(§ion.data, i));
+ }
+ if let Some(ref sky) = section.sky_light {
+ if sky.len() > i {
+ nbt::set_nibble(&mut sky_light, flat_index, nbt::get_nibble(sky, i));
+ }
+ }
+ if let Some(ref bl) = section.block_light {
+ if bl.len() > i {
+ nbt::set_nibble(&mut block_light, flat_index, nbt::get_nibble(bl, i));
+ }
+ }
+ }
+ }
+ }
+
+ (blocks, data, sky_light, block_light)
+}
+
+pub fn convert_chunk(
+ root_tag: &nbt::NbtCompound,
+ new_chunk_x: i32,
+ new_chunk_z: i32,
+ preserve_dynamic: bool,
+) -> Vec {
+ let level = build_legacy_chunk_level(root_tag, new_chunk_x, new_chunk_z, preserve_dynamic);
+ payload::encode_legacy_nbt(&level)
+}
+
+pub fn convert_chunk_for_save(
+ root_tag: &nbt::NbtCompound,
+ new_chunk_x: i32,
+ new_chunk_z: i32,
+ preserve_dynamic: bool,
+) -> Vec {
+ let level = build_legacy_chunk_level(root_tag, new_chunk_x, new_chunk_z, preserve_dynamic);
+ payload::encode_compressed_storage(&level)
+}
+
+fn build_legacy_chunk_level(
+ root_tag: &nbt::NbtCompound,
+ new_chunk_x: i32,
+ new_chunk_z: i32,
+ preserve_dynamic: bool,
+) -> nbt::NbtCompound {
+ let format_info = inspect_chunk(root_tag);
+ let source_level = root_tag
+ .compound("Level")
+ .unwrap_or(root_tag);
+ let is_modern = format_info.uses_modern_content_schema();
+ let (mut blocks, mut data, mut sky_light, mut block_light) =
+ if format_info.is_section_based() {
+ flatten_anvil_sections(source_level, &format_info, &mut None)
+ } else {
+ let b = get_byte_array_or_default(source_level, "Blocks", CHUNK_BLOCKS);
+ let d = get_byte_array_or_default(source_level, "Data", CHUNK_NIBBLES);
+ let s = get_byte_array_or_default(source_level, "SkyLight", CHUNK_NIBBLES);
+ let bl = get_byte_array_or_default(source_level, "BlockLight", CHUNK_NIBBLES);
+ (b, d, s, bl)
+ };
+
+ let height_map = get_byte_array_or_default(source_level, "HeightMap", HEIGHTMAP_SIZE);
+ let mut biomes = get_byte_array_or_default(source_level, "Biomes", BIOMES_SIZE);
+ if is_modern {
+ biomes.fill(1);
+ }
+
+ block_mapping::remap_blocks(&mut blocks, &mut data);
+ let last_update = source_level.long("LastUpdate").unwrap_or(0);
+ let inhabited_time = source_level.long("InhabitedTime").unwrap_or(0);
+ const TERRAIN_POPULATED_FLAGS: i16 = 0;
+ sky_light.fill(0);
+ block_light.fill(0);
+ let mut level = nbt::NbtCompound::new("Level");
+ level.insert("xPos", nbt::NbtValue::Int(new_chunk_x));
+ level.insert("zPos", nbt::NbtValue::Int(new_chunk_z));
+ level.insert("LastUpdate", nbt::NbtValue::Long(last_update));
+ level.insert("InhabitedTime", nbt::NbtValue::Long(inhabited_time));
+ level.insert("Blocks", nbt::NbtValue::ByteArray(blocks));
+ level.insert("Data", nbt::NbtValue::ByteArray(data));
+ level.insert("SkyLight", nbt::NbtValue::ByteArray(sky_light));
+ level.insert("BlockLight", nbt::NbtValue::ByteArray(block_light));
+ level.insert("HeightMap", nbt::NbtValue::ByteArray(height_map));
+ level.insert("TerrainPopulatedFlags", nbt::NbtValue::Short(TERRAIN_POPULATED_FLAGS));
+ level.insert("Biomes", nbt::NbtValue::ByteArray(biomes));
+ let source_chunk_x = source_level.int("xPos").unwrap_or(new_chunk_x);
+ let source_chunk_z = source_level.int("zPos").unwrap_or(new_chunk_z);
+ let block_offset_x = (source_chunk_x - new_chunk_x) * 16;
+ let block_offset_z = (source_chunk_z - new_chunk_z) * 16;
+ if preserve_dynamic && !is_modern {
+ let mut entities: Vec = source_level
+ .list("Entities")
+ .map(|l| l.to_vec())
+ .unwrap_or_default();
+ remap_entity_positions(&mut entities, block_offset_x, block_offset_z);
+ level.insert("Entities", nbt::NbtValue::List(entities));
+ } else {
+ level.insert("Entities", nbt::NbtValue::List(Vec::new()));
+ }
+
+ let mut tile_entities = if preserve_dynamic && !is_modern {
+ build_compatible_tile_entities(source_level, false)
+ } else {
+ build_safe_sign_tile_entities(source_level, is_modern)
+ };
+ remove_unsupported_tile_entities(&mut tile_entities);
+ remap_tile_entity_positions(&mut tile_entities, block_offset_x, block_offset_z);
+ level.insert("TileEntities", nbt::NbtValue::List(tile_entities));
+ if preserve_dynamic && !is_modern {
+ if let Some(ticks) = source_level.list("TileTicks") {
+ let remapped = remap_tile_tick_positions(ticks, block_offset_x, block_offset_z);
+ level.insert("TileTicks", nbt::NbtValue::List(remapped));
+ } else if let Some(ticks) = source_level.list("block_ticks") {
+ let remapped = remap_tile_tick_positions(ticks, block_offset_x, block_offset_z);
+ level.insert("TileTicks", nbt::NbtValue::List(remapped));
+ }
+ }
+
+ level
+}
+
+fn build_compatible_tile_entities(
+ source_level: &nbt::NbtCompound,
+ is_modern: bool,
+) -> Vec {
+ if !is_modern {
+ let tiles = source_level.list("TileEntities");
+ if let Some(t) = tiles {
+ return t.to_vec();
+ }
+ let tiles2 = source_level.list("block_entities");
+ if let Some(t) = tiles2 {
+ return t.to_vec();
+ }
+ return Vec::new();
+ }
+ build_safe_sign_tile_entities(source_level, true)
+}
+
+fn build_safe_sign_tile_entities(
+ source_level: &nbt::NbtCompound,
+ is_modern: bool,
+) -> Vec {
+ let mut result = Vec::new();
+ let source = if is_modern {
+ source_level
+ .list("block_entities")
+ .or_else(|| source_level.list("TileEntities"))
+ } else {
+ source_level
+ .list("TileEntities")
+ .or_else(|| source_level.list("block_entities"))
+ };
+
+ let source = match source {
+ Some(s) => s,
+ None => return result,
+ };
+
+ for tag in source {
+ if let nbt::NbtValue::Compound(block_entity) = tag {
+ let id = block_entity.string("id").unwrap_or("");
+ let is_sign = if is_modern {
+ is_modern_sign_tile_entity(id)
+ } else {
+ id == "Sign" || id == "minecraft:sign"
+ };
+ if !is_sign {
+ continue;
+ }
+ let sign = build_safe_legacy_sign_tile_entity(block_entity, is_modern);
+ result.push(nbt::NbtValue::Compound(sign));
+ }
+ }
+
+ result
+}
+
+fn is_modern_sign_tile_entity(id: &str) -> bool {
+ let stripped = id.strip_prefix("minecraft:").unwrap_or(id);
+ matches!(
+ stripped,
+ "sign"
+ | "hanging_sign"
+ | "oak_sign"
+ | "spruce_sign"
+ | "birch_sign"
+ | "jungle_sign"
+ | "acacia_sign"
+ | "dark_oak_sign"
+ | "mangrove_sign"
+ | "cherry_sign"
+ | "bamboo_sign"
+ | "crimson_sign"
+ | "warped_sign"
+ | "oak_hanging_sign"
+ | "spruce_hanging_sign"
+ | "birch_hanging_sign"
+ | "jungle_hanging_sign"
+ | "acacia_hanging_sign"
+ | "dark_oak_hanging_sign"
+ | "mangrove_hanging_sign"
+ | "cherry_hanging_sign"
+ | "bamboo_hanging_sign"
+ | "crimson_hanging_sign"
+ | "warped_hanging_sign"
+ )
+}
+
+fn read_tile_entity_coord(compound: &nbt::NbtCompound, name: &str) -> i32 {
+ compound.int(name)
+ .map(|v| v as i32)
+ .or_else(|| compound.short(name).map(|v| v as i32))
+ .or_else(|| compound.byte(name).map(|v| v as i32))
+ .unwrap_or(0)
+}
+
+fn extract_modern_sign_lines(block_entity: &nbt::NbtCompound) -> [String; 4] {
+ let mut lines: [String; 4] = Default::default();
+ if let Some(front_text) = block_entity.compound("front_text") {
+ if let Some(messages) = front_text.list("messages") {
+ for i in 0..4.min(messages.len()) {
+ if let nbt::NbtValue::String(s) = &messages[i] {
+ lines[i] = sanitize_legacy_sign_line(&simplify_sign_text(s));
+ }
+ }
+ return lines;
+ }
+ }
+
+ for i in 0..4 {
+ let key = format!("Text{}", i + 1);
+ let value = block_entity.string(&key).unwrap_or("");
+ lines[i] = sanitize_legacy_sign_line(&simplify_sign_text(value));
+ }
+
+ lines
+}
+
+fn extract_legacy_sign_lines(block_entity: &nbt::NbtCompound) -> [String; 4] {
+ let mut lines: [String; 4] = Default::default();
+ for i in 0..4 {
+ let key = format!("Text{}", i + 1);
+ let value = block_entity.string(&key).unwrap_or("");
+ lines[i] = sanitize_legacy_sign_line(&simplify_sign_text(value));
+ }
+ lines
+}
+
+fn sanitize_legacy_sign_line(raw: &str) -> String {
+ let mut builder = String::new();
+ for ch in raw.chars() {
+ if ch == '\r' || ch == '\n' || ch == '\t' || ch == '\0' {
+ continue;
+ }
+ if ch.is_control() {
+ continue;
+ }
+ builder.push(ch);
+ if builder.len() >= 15 {
+ break;
+ }
+ }
+ builder
+}
+
+fn simplify_sign_text(raw: &str) -> String {
+ let raw = raw.trim();
+ if !(raw.starts_with('{') || raw.starts_with('[') || raw.starts_with('"')) {
+ return raw.to_string();
+ }
+
+ if let Ok(doc) = serde_json::from_str::(raw) {
+ let mut builder = String::new();
+ append_json_text(&doc, &mut builder);
+ return builder;
+ }
+
+ raw.trim_matches('"').to_string()
+}
+
+fn append_json_text(element: &serde_json::Value, builder: &mut String) {
+ match element {
+ serde_json::Value::String(s) => builder.push_str(s),
+ serde_json::Value::Array(arr) => {
+ for item in arr {
+ append_json_text(item, builder);
+ }
+ }
+ serde_json::Value::Object(obj) => {
+ if let Some(serde_json::Value::String(text)) = obj.get("text") {
+ builder.push_str(text);
+ }
+ if let Some(extra) = obj.get("extra") {
+ append_json_text(extra, builder);
+ }
+ }
+ _ => {}
+ }
+}
+
+fn build_safe_legacy_sign_tile_entity(
+ block_entity: &nbt::NbtCompound,
+ is_modern: bool,
+) -> nbt::NbtCompound {
+ let x = read_tile_entity_coord(block_entity, "x");
+ let y = read_tile_entity_coord(block_entity, "y");
+ let z = read_tile_entity_coord(block_entity, "z");
+ let lines = if is_modern {
+ extract_modern_sign_lines(block_entity)
+ } else {
+ extract_legacy_sign_lines(block_entity)
+ };
+
+ let mut sign = nbt::NbtCompound::new("");
+ sign.insert("id", nbt::NbtValue::String("Sign".to_string()));
+ sign.insert("x", nbt::NbtValue::Int(x));
+ sign.insert("y", nbt::NbtValue::Int(y));
+ sign.insert("z", nbt::NbtValue::Int(z));
+ sign.insert("Text1", nbt::NbtValue::String(lines[0].clone()));
+ sign.insert("Text2", nbt::NbtValue::String(lines[1].clone()));
+ sign.insert("Text3", nbt::NbtValue::String(lines[2].clone()));
+ sign.insert("Text4", nbt::NbtValue::String(lines[3].clone()));
+ sign
+}
+
+fn remap_tile_tick_positions(
+ ticks: &[nbt::NbtValue],
+ block_offset_x: i32,
+ block_offset_z: i32,
+) -> Vec {
+ if block_offset_x == 0 && block_offset_z == 0 {
+ return ticks.to_vec();
+ }
+ let mut result = Vec::new();
+ for tag in ticks {
+ if let nbt::NbtValue::Compound(mut tick) = tag.clone() {
+ if let Some(nbt::NbtValue::Int(x)) = tick.get_mut("x") {
+ *x -= block_offset_x;
+ }
+ if let Some(nbt::NbtValue::Int(z)) = tick.get_mut("z") {
+ *z -= block_offset_z;
+ }
+ result.push(nbt::NbtValue::Compound(tick));
+ }
+ }
+ result
+}
+
+fn remap_entity_positions(entities: &mut Vec, block_offset_x: i32, block_offset_z: i32) {
+ if block_offset_x == 0 && block_offset_z == 0 {
+ return;
+ }
+ for tag in entities.iter_mut() {
+ if let nbt::NbtValue::Compound(entity) = tag {
+ if let Some(nbt::NbtValue::List(pos)) = entity.get_mut("Pos") {
+ if pos.len() >= 3 {
+ if let nbt::NbtValue::Double(x) = &mut pos[0] {
+ *x -= block_offset_x as f64;
+ }
+ if let nbt::NbtValue::Double(z) = &mut pos[2] {
+ *z -= block_offset_z as f64;
+ }
+ }
+ }
+ if let Some(nbt::NbtValue::Compound(riding)) = entity.get_mut("Riding") {
+ let mut inner = vec![nbt::NbtValue::Compound(riding.clone())];
+ remap_entity_positions(&mut inner, block_offset_x, block_offset_z);
+ if let Some(nbt::NbtValue::Compound(updated)) = inner.into_iter().next() {
+ *riding = updated;
+ }
+ }
+ }
+ }
+}
+
+fn remove_unsupported_tile_entities(tile_entities: &mut Vec) {
+ tile_entities.retain(|tag| {
+ if let nbt::NbtValue::Compound(te) = tag {
+ let id = te.string("id").unwrap_or("");
+ return id != "Control" && id != "minecraft:command_block" && id != "CommandBlock";
+ }
+ false
+ });
+}
+
+fn remap_tile_entity_positions(tile_entities: &mut Vec, block_offset_x: i32, block_offset_z: i32) {
+ if block_offset_x == 0 && block_offset_z == 0 {
+ return;
+ }
+ for tag in tile_entities.iter_mut() {
+ if let nbt::NbtValue::Compound(te) = tag {
+ if let Some(nbt::NbtValue::Int(x)) = te.get_mut("x") {
+ *x -= block_offset_x;
+ }
+ if let Some(nbt::NbtValue::Int(z)) = te.get_mut("z") {
+ *z -= block_offset_z;
+ }
+ }
+ }
+}
+
+pub fn build_modern_anvil_level(
+ legacy_level: &nbt::NbtCompound,
+ chunk_x: i32,
+ chunk_z: i32,
+) -> nbt::NbtCompound {
+ let default_blocks = vec![0u8; 32768];
+ let default_data = vec![0u8; 16384];
+ let old_blocks_ref = legacy_level
+ .byte_array("Blocks")
+ .unwrap_or(&default_blocks);
+ let old_data_ref = legacy_level
+ .byte_array("Data")
+ .unwrap_or(&default_data);
+
+ let mut section_tags: Vec = Vec::new();
+ for section_y in 0..8 {
+ let mut palette_list: Vec = Vec::new();
+ let mut palette_dict: HashMap = HashMap::new();
+ let mut indices = [0i32; 4096];
+ let mut has_non_air = false;
+ for y in 0..16 {
+ for z in 0..16 {
+ for x in 0..16 {
+ let global_y = section_y * 16 + y;
+ let legacy_flat_index = ((x * 16) + z) * 128 + global_y;
+ if legacy_flat_index >= old_blocks_ref.len() {
+ continue;
+ }
+ let block_id = old_blocks_ref[legacy_flat_index];
+ let meta = nbt::get_nibble(old_data_ref, legacy_flat_index);
+ let modern_state = get_contextual_block_state(block_id, meta, x, global_y, z, old_blocks_ref, old_data_ref);
+ if modern_state != "minecraft:air" {
+ has_non_air = true;
+ }
+
+ let idx = *palette_dict
+ .entry(modern_state.clone())
+ .or_insert_with(|| {
+ let idx = palette_list.len();
+ palette_list.push(modern_state);
+ idx
+ });
+
+ indices[y * 256 + z * 16 + x] = idx as i32;
+ }
+ }
+ }
+
+ if !has_non_air && section_y > 0 {
+ continue;
+ }
+
+ let mut section_tag = nbt::NbtCompound::new("");
+ section_tag.insert("Y", nbt::NbtValue::Byte(section_y as i8));
+ let mut block_states_tag = nbt::NbtCompound::new("block_states");
+ let mut nbt_palette: Vec = Vec::new();
+ for state in &palette_list {
+ let mut p_tag = nbt::NbtCompound::new("");
+ if let Some(bracket_idx) = state.find('[') {
+ let name = &state[..bracket_idx];
+ let props_str = &state[bracket_idx + 1..state.len() - 1];
+ p_tag.insert("Name", nbt::NbtValue::String(name.to_string()));
+ let mut props_compound = nbt::NbtCompound::new("Properties");
+ for prop in props_str.split(',') {
+ let kv: Vec<&str> = prop.splitn(2, '=').collect();
+ if kv.len() == 2 {
+ props_compound
+ .insert(kv[0], nbt::NbtValue::String(kv[1].to_string()));
+ }
+ }
+ p_tag.insert("Properties", nbt::NbtValue::Compound(props_compound));
+ } else {
+ p_tag.insert("Name", nbt::NbtValue::String(state.to_string()));
+ }
+ nbt_palette.push(nbt::NbtValue::Compound(p_tag));
+ }
+ block_states_tag.insert("palette", nbt::NbtValue::List(nbt_palette));
+ if palette_list.len() > 1 {
+ let bits_per_block = (64.0 / (palette_list.len() as f64).log2().ceil()).floor().max(4.0) as usize;
+ let values_per_long = 64 / bits_per_block;
+ let long_count = ((4096.0 / values_per_long as f64).ceil()) as usize;
+ let mut data_array = vec![0i64; long_count];
+ let mut current_long: u64 = 0;
+ let mut blocks_in_current_long = 0;
+ let mut long_index = 0;
+ for i in 0..4096 {
+ let val = indices[i] as u64;
+ current_long |= val << (blocks_in_current_long * bits_per_block);
+ blocks_in_current_long += 1;
+ if blocks_in_current_long >= values_per_long {
+ data_array[long_index] = current_long as i64;
+ long_index += 1;
+ current_long = 0;
+ blocks_in_current_long = 0;
+ }
+ }
+
+ if blocks_in_current_long > 0 {
+ data_array[long_index] = current_long as i64;
+ }
+
+ block_states_tag.insert("data", nbt::NbtValue::LongArray(data_array));
+ }
+
+ section_tag.insert("block_states", nbt::NbtValue::Compound(block_states_tag));
+ let mut biomes_tag = nbt::NbtCompound::new("biomes");
+ let biome_palette = nbt::NbtValue::List(vec![nbt::NbtValue::String(
+ "minecraft:plains".to_string(),
+ )]);
+ biomes_tag.insert("palette", biome_palette);
+ section_tag.insert("biomes", nbt::NbtValue::Compound(biomes_tag));
+ section_tags.push(nbt::NbtValue::Compound(section_tag));
+ }
+
+ let mut root = nbt::NbtCompound::new("");
+ root.insert("xPos", nbt::NbtValue::Int(chunk_x));
+ root.insert("zPos", nbt::NbtValue::Int(chunk_z));
+ root.insert("yPos", nbt::NbtValue::Int(0));
+ root.insert("Status", nbt::NbtValue::String("full".to_string()));
+ let mut block_entities: Vec = Vec::new();
+ if let Some(old_te_list) = legacy_level.list("TileEntities") {
+ for old_te in old_te_list {
+ if let nbt::NbtValue::Compound(old_te_comp) = old_te {
+ let mut new_te = old_te_comp.clone();
+ let id = new_te.string("id").unwrap_or("").to_string();
+ let new_id = match id.as_str() {
+ "Chest" => Some("minecraft:chest"),
+ "Furnace" => Some("minecraft:furnace"),
+ "BrewingStand" => Some("minecraft:brewing_stand"),
+ "EnchantTable" => Some("minecraft:enchanting_table"),
+ "Trap" => Some("minecraft:dispenser"),
+ "MobSpawner" => Some("minecraft:mob_spawner"),
+ "Control" => Some("minecraft:command_block"),
+ "Beacon" => Some("minecraft:beacon"),
+ "Skull" => Some("minecraft:skull"),
+ "Sign" => Some("minecraft:sign"),
+ "Cauldron" => Some("minecraft:cauldron"),
+ "Dropper" => Some("minecraft:dropper"),
+ "Hopper" => Some("minecraft:hopper"),
+ "Comparator" => Some("minecraft:comparator"),
+ "RecordPlayer" => Some("minecraft:jukebox"),
+ "Banner" => Some("minecraft:banner"),
+ _ if id.starts_with("minecraft:") => Some(id.as_str()),
+ _ => None,
+ };
+
+ if let Some(nid) = new_id {
+ new_te.insert("id", nbt::NbtValue::String(nid.to_string()));
+ block_entities.push(nbt::NbtValue::Compound(new_te));
+ }
+ }
+ }
+ }
+
+ for i in 0..old_blocks_ref.len().min(32768) {
+ if old_blocks_ref[i] == 26 {
+ let y = i % 128;
+ let z = (i / 128) % 16;
+ let x = i / 2048;
+ let mut bed_te = nbt::NbtCompound::new("");
+ bed_te.insert("id", nbt::NbtValue::String("minecraft:bed".to_string()));
+ bed_te.insert("color", nbt::NbtValue::Int(14));
+ bed_te.insert("x", nbt::NbtValue::Int(chunk_x * 16 + x as i32));
+ bed_te.insert("y", nbt::NbtValue::Int(y as i32));
+ bed_te.insert("z", nbt::NbtValue::Int(chunk_z * 16 + z as i32));
+ block_entities.push(nbt::NbtValue::Compound(bed_te));
+ }
+ }
+
+ root.insert(
+ "block_entities",
+ nbt::NbtValue::List(block_entities),
+ );
+ root.insert("sections", nbt::NbtValue::List(section_tags));
+ root
+}
+
+fn get_contextual_block_state(
+ block_id: u8,
+ meta: u8,
+ x: usize,
+ global_y: usize,
+ z: usize,
+ old_blocks: &[u8],
+ old_data: &[u8],
+) -> String {
+ let modern_state = get_modern_block_name(block_id, meta);
+ let get_neighbor = |nx: i32, ny: i32, nz: i32| -> (u8, u8) {
+ if nx < 0 || nx > 15 || ny < 0 || ny > 127 || nz < 0 || nz > 15 {
+ return (0, 0);
+ }
+ let idx = ((nx as usize * 16) + nz as usize) * 128 + ny as usize;
+ if idx >= old_blocks.len() {
+ return (0, 0);
+ }
+ let n_id = old_blocks[idx];
+ let n_meta = nbt::get_nibble(old_data, idx);
+ (n_id, n_meta)
+ };
+
+ let bracket_index = modern_state.find('[');
+ let name = if let Some(bi) = bracket_index {
+ &modern_state[..bi]
+ } else {
+ &modern_state
+ };
+ let mut properties: HashMap = HashMap::new();
+ if let Some(bi) = bracket_index {
+ let props_str = &modern_state[bi + 1..modern_state.len() - 1];
+ for prop in props_str.split(',') {
+ let kv: Vec<&str> = prop.splitn(2, '=').collect();
+ if kv.len() == 2 {
+ properties.insert(kv[0].to_string(), kv[1].to_string());
+ }
+ }
+ }
+
+ let mut properties_changed = false;
+ if block_id == 64 || block_id == 71 || (block_id >= 193 && block_id <= 197) {
+ let is_top = (meta & 8) == 8;
+ properties.insert("half".to_string(), if is_top { "upper" } else { "lower" }.to_string());
+ if is_top {
+ properties.insert(
+ "hinge".to_string(),
+ if (meta & 1) == 1 { "right" } else { "left" }.to_string(),
+ );
+ let (bottom_id, bottom_meta) = get_neighbor(x as i32, global_y as i32 - 1, z as i32);
+ if bottom_id == block_id {
+ let facing_meta = bottom_meta & 3;
+ properties.insert(
+ "facing".to_string(),
+ match facing_meta {
+ 0 => "east",
+ 1 => "south",
+ 2 => "west",
+ _ => "north",
+ }
+ .to_string(),
+ );
+ properties.insert(
+ "open".to_string(),
+ if (bottom_meta & 4) == 4 { "true" } else { "false" }.to_string(),
+ );
+ } else {
+ properties.insert("facing".to_string(), "east".to_string());
+ properties.insert("open".to_string(), "false".to_string());
+ }
+ } else {
+ let facing_meta = meta & 3;
+ properties.insert(
+ "facing".to_string(),
+ match facing_meta {
+ 0 => "east",
+ 1 => "south",
+ 2 => "west",
+ _ => "north",
+ }
+ .to_string(),
+ );
+ properties.insert(
+ "open".to_string(),
+ if (meta & 4) == 4 { "true" } else { "false" }.to_string(),
+ );
+ let (top_id, top_meta) = get_neighbor(x as i32, global_y as i32 + 1, z as i32);
+ properties.insert(
+ "hinge".to_string(),
+ if top_id == block_id {
+ if (top_meta & 1) == 1 { "right" } else { "left" }
+ } else {
+ "left"
+ }
+ .to_string(),
+ );
+ }
+ properties_changed = true;
+ } else if block_id == 63 || block_id == 68 {
+ if block_id == 68 {
+ properties.insert(
+ "facing".to_string(),
+ match meta {
+ 2 => "north",
+ 3 => "south",
+ 4 => "west",
+ _ => "east",
+ }
+ .to_string(),
+ );
+ } else {
+ properties.insert("rotation".to_string(), meta.to_string());
+ }
+ properties.remove("waterlogged");
+ properties_changed = true;
+ } else if block_id == 50 || block_id == 75 || block_id == 76 {
+ if meta >= 1 && meta <= 4 {
+ properties.insert(
+ "facing".to_string(),
+ match meta {
+ 1 => "east",
+ 2 => "west",
+ 3 => "south",
+ _ => "north",
+ }
+ .to_string(),
+ );
+ }
+ if block_id == 76 {
+ properties.insert("lit".to_string(), "true".to_string());
+ } else if block_id == 75 {
+ properties.insert("lit".to_string(), "false".to_string());
+ }
+ properties_changed = true;
+ } else if block_id == 54 || block_id == 146 {
+ let facing = match meta {
+ 2 => "north",
+ 3 => "south",
+ 4 => "west",
+ 5 => "east",
+ _ => "south",
+ };
+ properties.insert("type".to_string(), "single".to_string());
+ properties.insert("facing".to_string(), facing.to_string());
+ let dirs = [(-1i32, 0i32), (1, 0), (0, -1), (0, 1)];
+ for (dx, dz) in dirs {
+ let (nid, _) = get_neighbor(x as i32 + dx, global_y as i32, z as i32 + dz);
+ if nid == block_id {
+ let is_left = match facing {
+ "north" => dx == -1,
+ "south" => dx == 1,
+ "west" => dz == 1,
+ "east" => dz == -1,
+ _ => false,
+ };
+ properties.insert(
+ "type".to_string(),
+ if is_left { "right" } else { "left" }.to_string(),
+ );
+ break;
+ }
+ }
+ properties_changed = true;
+ } else if is_stair_legacy(block_id) {
+ let facing = get_stair_facing(meta);
+ let half = if (meta & STAIR_UPSIDE_DOWN_BIT) != 0 {
+ "top"
+ } else {
+ "bottom"
+ };
+ properties.insert("facing".to_string(), facing.to_string());
+ properties.insert("half".to_string(), half.to_string());
+ properties.insert("shape".to_string(), "straight".to_string());
+ let (front_dx, front_dz) = match facing {
+ "north" => (0i32, 1i32),
+ "south" => (0, -1),
+ "west" => (1, 0),
+ "east" => (-1, 0),
+ _ => (0, 0),
+ };
+ let (back_dx, back_dz) = match facing {
+ "north" => (0i32, -1i32),
+ "south" => (0, 1),
+ "west" => (-1, 0),
+ "east" => (1, 0),
+ _ => (0, 0),
+ };
+
+ let back_neighbor = get_neighbor(x as i32 + back_dx, global_y as i32, z as i32 + back_dz);
+ if is_stair_legacy(back_neighbor.0) {
+ let back_facing = get_stair_facing(back_neighbor.1);
+ let back_half = if (back_neighbor.1 & STAIR_UPSIDE_DOWN_BIT) != 0 {
+ "top"
+ } else {
+ "bottom"
+ };
+ if back_half == half {
+ if let Some(side) = get_relative_side(facing, &back_facing) {
+ properties.insert("shape".to_string(), format!("outer_{}", side));
+ }
+ }
+ } else {
+ let front_neighbor = get_neighbor(x as i32 + front_dx, global_y as i32, z as i32 + front_dz);
+ if is_stair_legacy(front_neighbor.0) {
+ let front_facing = get_stair_facing(front_neighbor.1);
+ let front_half = if (front_neighbor.1 & STAIR_UPSIDE_DOWN_BIT) != 0 {
+ "top"
+ } else {
+ "bottom"
+ };
+ if front_half == half {
+ if let Some(side) = get_relative_side(facing, &front_facing) {
+ properties.insert("shape".to_string(), format!("inner_{}", side));
+ }
+ }
+ }
+ }
+ properties_changed = true;
+ } else if is_fence_legacy(block_id) {
+ let (n_id, _) = get_neighbor(x as i32, global_y as i32, z as i32 - 1);
+ let (e_id, _) = get_neighbor(x as i32 + 1, global_y as i32, z as i32);
+ let (s_id, _) = get_neighbor(x as i32, global_y as i32, z as i32 + 1);
+ let (w_id, _) = get_neighbor(x as i32 - 1, global_y as i32, z as i32);
+ properties.insert(
+ "north".to_string(),
+ (is_fence_legacy(n_id) || is_fence_gate_legacy(n_id) || is_likely_solid_attachment(n_id))
+ .to_string(),
+ );
+ properties.insert(
+ "east".to_string(),
+ (is_fence_legacy(e_id) || is_fence_gate_legacy(e_id) || is_likely_solid_attachment(e_id))
+ .to_string(),
+ );
+ properties.insert(
+ "south".to_string(),
+ (is_fence_legacy(s_id) || is_fence_gate_legacy(s_id) || is_likely_solid_attachment(s_id))
+ .to_string(),
+ );
+ properties.insert(
+ "west".to_string(),
+ (is_fence_legacy(w_id) || is_fence_gate_legacy(w_id) || is_likely_solid_attachment(w_id))
+ .to_string(),
+ );
+ properties_changed = true;
+ } else if is_pane_legacy(block_id) {
+ let (n_id, _) = get_neighbor(x as i32, global_y as i32, z as i32 - 1);
+ let (e_id, _) = get_neighbor(x as i32 + 1, global_y as i32, z as i32);
+ let (s_id, _) = get_neighbor(x as i32, global_y as i32, z as i32 + 1);
+ let (w_id, _) = get_neighbor(x as i32 - 1, global_y as i32, z as i32);
+ properties.insert(
+ "north".to_string(),
+ (is_pane_legacy(n_id) || is_glass_legacy(n_id) || is_likely_solid_attachment(n_id))
+ .to_string(),
+ );
+ properties.insert(
+ "east".to_string(),
+ (is_pane_legacy(e_id) || is_glass_legacy(e_id) || is_likely_solid_attachment(e_id))
+ .to_string(),
+ );
+ properties.insert(
+ "south".to_string(),
+ (is_pane_legacy(s_id) || is_glass_legacy(s_id) || is_likely_solid_attachment(s_id))
+ .to_string(),
+ );
+ properties.insert(
+ "west".to_string(),
+ (is_pane_legacy(w_id) || is_glass_legacy(w_id) || is_likely_solid_attachment(w_id))
+ .to_string(),
+ );
+ properties_changed = true;
+ }
+
+ if properties_changed || name == "minecraft:red_bed" {
+ if !properties.is_empty() {
+ let parts: Vec = properties.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
+ return format!("{}[{}]", name, parts.join(","));
+ }
+ return name.to_string();
+ }
+
+ modern_state
+}
+
+fn get_modern_block_name(block_id: u8, meta: u8) -> String {
+ let name = get_contextual_modern_name(block_id, meta);
+ if let Some(props) = get_contextual_properties(block_id, meta) {
+ format!("{}[{}]", name, props)
+ } else {
+ name
+ }
+}
+
+fn get_contextual_modern_name(block_id: u8, meta: u8) -> String {
+ match block_id {
+ 0 => "minecraft:air".into(),
+ 1 => "minecraft:stone".into(),
+ 2 => "minecraft:grass_block".into(),
+ 3 => "minecraft:dirt".into(),
+ 4 => "minecraft:cobblestone".into(),
+ 5 => match meta {
+ 1 => "minecraft:spruce_planks",
+ 2 => "minecraft:birch_planks",
+ 3 => "minecraft:jungle_planks",
+ _ => "minecraft:oak_planks",
+ }
+ .into(),
+ 6 => match meta {
+ 1 => "minecraft:spruce_sapling",
+ 2 => "minecraft:birch_sapling",
+ 3 => "minecraft:jungle_sapling",
+ _ => "minecraft:oak_sapling",
+ }
+ .into(),
+ 7 => "minecraft:bedrock".into(),
+ 8 | 9 => "minecraft:water".into(),
+ 10 | 11 => "minecraft:lava".into(),
+ 12 => "minecraft:sand".into(),
+ 13 => "minecraft:gravel".into(),
+ 14 => "minecraft:gold_ore".into(),
+ 15 => "minecraft:iron_ore".into(),
+ 16 => "minecraft:coal_ore".into(),
+ 17 => match meta & 3 {
+ 1 => "minecraft:spruce_log",
+ 2 => "minecraft:birch_log",
+ 3 => "minecraft:jungle_log",
+ _ => "minecraft:oak_log",
+ }
+ .into(),
+ 18 => match meta & 3 {
+ 1 => "minecraft:spruce_leaves",
+ 2 => "minecraft:birch_leaves",
+ 3 => "minecraft:jungle_leaves",
+ _ => "minecraft:oak_leaves",
+ }
+ .into(),
+ 19 => "minecraft:sponge".into(),
+ 20 => "minecraft:glass".into(),
+ 21 => "minecraft:lapis_ore".into(),
+ 22 => "minecraft:lapis_block".into(),
+ 23 => "minecraft:dispenser".into(),
+ 24 => match meta {
+ 1 => "minecraft:chiseled_sandstone",
+ 2 => "minecraft:smooth_sandstone",
+ _ => "minecraft:sandstone",
+ }
+ .into(),
+ 25 => "minecraft:note_block".into(),
+ 26 => "minecraft:red_bed".into(),
+ 27 => "minecraft:powered_rail".into(),
+ 28 => "minecraft:detector_rail".into(),
+ 29 => "minecraft:sticky_piston".into(),
+ 30 => "minecraft:cobweb".into(),
+ 31 => "minecraft:short_grass".into(),
+ 32 => "minecraft:dead_bush".into(),
+ 33 => "minecraft:piston".into(),
+ 35 => match meta {
+ 1 => "minecraft:orange_wool",
+ 2 => "minecraft:magenta_wool",
+ 3 => "minecraft:light_blue_wool",
+ 4 => "minecraft:yellow_wool",
+ 5 => "minecraft:lime_wool",
+ 6 => "minecraft:pink_wool",
+ 7 => "minecraft:gray_wool",
+ 8 => "minecraft:light_gray_wool",
+ 9 => "minecraft:cyan_wool",
+ 10 => "minecraft:purple_wool",
+ 11 => "minecraft:blue_wool",
+ 12 => "minecraft:brown_wool",
+ 13 => "minecraft:green_wool",
+ 14 => "minecraft:red_wool",
+ 15 => "minecraft:black_wool",
+ _ => "minecraft:white_wool",
+ }
+ .into(),
+ 37 => "minecraft:dandelion".into(),
+ 38 => "minecraft:poppy".into(),
+ 39 => "minecraft:brown_mushroom".into(),
+ 40 => "minecraft:red_mushroom".into(),
+ 41 => "minecraft:gold_block".into(),
+ 42 => "minecraft:iron_block".into(),
+ 44 => {
+ let variant = meta & 7;
+ let name = match variant {
+ 1 => "minecraft:sandstone_slab",
+ 2 => "minecraft:stone_slab",
+ 3 => "minecraft:cobblestone_slab",
+ 4 => "minecraft:brick_slab",
+ 5 => "minecraft:stone_brick_slab",
+ 6 => "minecraft:nether_brick_slab",
+ 7 => "minecraft:quartz_slab",
+ _ => "minecraft:stone_slab",
+ };
+ return name.to_string();
+ }
+ 45 => "minecraft:bricks".into(),
+ 46 => "minecraft:tnt".into(),
+ 47 => "minecraft:bookshelf".into(),
+ 48 => "minecraft:mossy_cobblestone".into(),
+ 49 => "minecraft:obsidian".into(),
+ 50 => "minecraft:torch".into(),
+ 51 => "minecraft:fire".into(),
+ 52 => "minecraft:mob_spawner".into(),
+ 53 => "minecraft:oak_stairs".into(),
+ 54 => "minecraft:chest".into(),
+ 55 => "minecraft:redstone_wire".into(),
+ 56 => "minecraft:diamond_ore".into(),
+ 57 => "minecraft:diamond_block".into(),
+ 58 => "minecraft:crafting_table".into(),
+ 59 => "minecraft:wheat".into(),
+ 60 => "minecraft:farmland".into(),
+ 61 => "minecraft:furnace".into(),
+ 63 => "minecraft:oak_sign".into(),
+ 64 => "minecraft:oak_door".into(),
+ 65 => "minecraft:ladder".into(),
+ 66 => "minecraft:rail".into(),
+ 67 => "minecraft:cobblestone_stairs".into(),
+ 68 => "minecraft:oak_wall_sign".into(),
+ 69 => "minecraft:lever".into(),
+ 70 => "minecraft:stone_pressure_plate".into(),
+ 71 => "minecraft:iron_door".into(),
+ 72 => "minecraft:oak_pressure_plate".into(),
+ 73 => "minecraft:redstone_ore".into(),
+ 75 => "minecraft:redstone_wall_torch".into(),
+ 76 => "minecraft:redstone_torch".into(),
+ 77 => "minecraft:stone_button".into(),
+ 78 => "minecraft:snow".into(),
+ 79 => "minecraft:ice".into(),
+ 80 => "minecraft:snow_block".into(),
+ 81 => "minecraft:cactus".into(),
+ 82 => "minecraft:clay".into(),
+ 83 => "minecraft:sugar_cane".into(),
+ 84 => "minecraft:jukebox".into(),
+ 85 => "minecraft:oak_fence".into(),
+ 86 => "minecraft:carved_pumpkin".into(),
+ 87 => "minecraft:netherrack".into(),
+ 88 => "minecraft:soul_sand".into(),
+ 89 => "minecraft:glowstone".into(),
+ 90 => "minecraft:nether_portal".into(),
+ 91 => "minecraft:jack_o_lantern".into(),
+ 92 => "minecraft:cake".into(),
+ 93 => "minecraft:unpowered_repeater".into(),
+ 94 => "minecraft:powered_repeater".into(),
+ 96 => "minecraft:trapdoor".into(),
+ 98 => "minecraft:stone_bricks".into(),
+ 101 => "minecraft:iron_bars".into(),
+ 102 => "minecraft:glass_pane".into(),
+ 103 => "minecraft:melon".into(),
+ 104 => "minecraft:pumpkin_stem".into(),
+ 105 => "minecraft:melon_stem".into(),
+ 106 => "minecraft:vine".into(),
+ 107 => "minecraft:oak_fence_gate".into(),
+ 108 => "minecraft:brick_stairs".into(),
+ 109 => "minecraft:stone_brick_stairs".into(),
+ 110 => "minecraft:mycelium".into(),
+ 111 => "minecraft:lily_pad".into(),
+ 112 => "minecraft:nether_bricks".into(),
+ 113 => "minecraft:nether_brick_fence".into(),
+ 114 => "minecraft:nether_brick_stairs".into(),
+ 115 => "minecraft:nether_wart".into(),
+ 116 => "minecraft:enchanting_table".into(),
+ 117 => "minecraft:brewing_stand".into(),
+ 118 => "minecraft:cauldron".into(),
+ 121 => "minecraft:end_stone".into(),
+ 122 => "minecraft:dragon_egg".into(),
+ 123 => "minecraft:redstone_lamp".into(),
+ 124 => "minecraft:redstone_lamp".into(),
+ 125 => "minecraft:double_wooden_slab".into(),
+ 126 => "minecraft:wooden_slab".into(),
+ 127 => "minecraft:cocoa".into(),
+ 128 => "minecraft:sandstone_stairs".into(),
+ 129 => "minecraft:emerald_ore".into(),
+ 130 => "minecraft:ender_chest".into(),
+ 131 => "minecraft:tripwire_hook".into(),
+ 132 => "minecraft:tripwire".into(),
+ 133 => "minecraft:emerald_block".into(),
+ 134 => "minecraft:spruce_stairs".into(),
+ 135 => "minecraft:birch_stairs".into(),
+ 136 => "minecraft:jungle_stairs".into(),
+ 138 => "minecraft:beacon".into(),
+ 139 => "minecraft:cobblestone_wall".into(),
+ 140 => "minecraft:flower_pot".into(),
+ 141 => "minecraft:carrots".into(),
+ 142 => "minecraft:potatoes".into(),
+ 143 => "minecraft:wooden_button".into(),
+ 144 => "minecraft:skull".into(),
+ 145 => "minecraft:anvil".into(),
+ 146 => "minecraft:trapped_chest".into(),
+ 147 => "minecraft:light_weighted_pressure_plate".into(),
+ 148 => "minecraft:heavy_weighted_pressure_plate".into(),
+ 149 => "minecraft:unpowered_comparator".into(),
+ 150 => "minecraft:powered_comparator".into(),
+ 151 => "minecraft:daylight_detector".into(),
+ 152 => "minecraft:redstone_block".into(),
+ 153 => "minecraft:nether_quartz_ore".into(),
+ 154 => "minecraft:hopper".into(),
+ 155 => match meta {
+ 1 => "minecraft:chiseled_quartz_block",
+ 2 => "minecraft:quartz_pillar",
+ 3 => "minecraft:quartz_pillar",
+ _ => "minecraft:quartz_block",
+ }
+ .into(),
+ 156 => "minecraft:quartz_stairs".into(),
+ 157 => "minecraft:activator_rail".into(),
+ 158 => "minecraft:dropper".into(),
+ 159 => match meta {
+ 0 => "minecraft:white_stained_hardened_clay",
+ 1 => "minecraft:orange_stained_hardened_clay",
+ 2 => "minecraft:magenta_stained_hardened_clay",
+ 3 => "minecraft:light_blue_stained_hardened_clay",
+ 4 => "minecraft:yellow_stained_hardened_clay",
+ 5 => "minecraft:lime_stained_hardened_clay",
+ 6 => "minecraft:pink_stained_hardened_clay",
+ 7 => "minecraft:gray_stained_hardened_clay",
+ 8 => "minecraft:light_gray_stained_hardened_clay",
+ 9 => "minecraft:cyan_stained_hardened_clay",
+ 10 => "minecraft:purple_stained_hardened_clay",
+ 11 => "minecraft:blue_stained_hardened_clay",
+ 12 => "minecraft:brown_stained_hardened_clay",
+ 13 => "minecraft:green_stained_hardened_clay",
+ 14 => "minecraft:red_stained_hardened_clay",
+ 15 => "minecraft:black_stained_hardened_clay",
+ _ => "minecraft:white_stained_hardened_clay",
+ }
+ .into(),
+ 160 => match meta {
+ 0 => "minecraft:white_stained_glass_pane",
+ 1 => "minecraft:orange_stained_glass_pane",
+ 2 => "minecraft:magenta_stained_glass_pane",
+ 3 => "minecraft:light_blue_stained_glass_pane",
+ 4 => "minecraft:yellow_stained_glass_pane",
+ 5 => "minecraft:lime_stained_glass_pane",
+ 6 => "minecraft:pink_stained_glass_pane",
+ 7 => "minecraft:gray_stained_glass_pane",
+ 8 => "minecraft:light_gray_stained_glass_pane",
+ 9 => "minecraft:cyan_stained_glass_pane",
+ 10 => "minecraft:purple_stained_glass_pane",
+ 11 => "minecraft:blue_stained_glass_pane",
+ 12 => "minecraft:brown_stained_glass_pane",
+ 13 => "minecraft:green_stained_glass_pane",
+ 14 => "minecraft:red_stained_glass_pane",
+ 15 => "minecraft:black_stained_glass_pane",
+ _ => "minecraft:white_stained_glass_pane",
+ }
+ .into(),
+ 161 => match meta & 3 {
+ 1 => "minecraft:acacia_leaves",
+ _ => "minecraft:dark_oak_leaves",
+ }
+ .into(),
+ 162 => match meta & 3 {
+ 1 => "minecraft:acacia_log",
+ _ => "minecraft:dark_oak_log",
+ }
+ .into(),
+ 163 => "minecraft:acacia_stairs".into(),
+ 164 => "minecraft:dark_oak_stairs".into(),
+ 170 => "minecraft:hay_block".into(),
+ 171 => match meta {
+ 0 => "minecraft:white_carpet",
+ 1 => "minecraft:orange_carpet",
+ 2 => "minecraft:magenta_carpet",
+ 3 => "minecraft:light_blue_carpet",
+ 4 => "minecraft:yellow_carpet",
+ 5 => "minecraft:lime_carpet",
+ 6 => "minecraft:pink_carpet",
+ 7 => "minecraft:gray_carpet",
+ 8 => "minecraft:light_gray_carpet",
+ 9 => "minecraft:cyan_carpet",
+ 10 => "minecraft:purple_carpet",
+ 11 => "minecraft:blue_carpet",
+ 12 => "minecraft:brown_carpet",
+ 13 => "minecraft:green_carpet",
+ 14 => "minecraft:red_carpet",
+ 15 => "minecraft:black_carpet",
+ _ => "minecraft:white_carpet",
+ }
+ .into(),
+ 172 => match meta {
+ 0 => "minecraft:white_concrete",
+ 1 => "minecraft:orange_concrete",
+ 2 => "minecraft:magenta_concrete",
+ 3 => "minecraft:light_blue_concrete",
+ 4 => "minecraft:yellow_concrete",
+ 5 => "minecraft:lime_concrete",
+ 6 => "minecraft:pink_concrete",
+ 7 => "minecraft:gray_concrete",
+ 8 => "minecraft:light_gray_concrete",
+ 9 => "minecraft:cyan_concrete",
+ 10 => "minecraft:purple_concrete",
+ 11 => "minecraft:blue_concrete",
+ 12 => "minecraft:brown_concrete",
+ 13 => "minecraft:green_concrete",
+ 14 => "minecraft:red_concrete",
+ 15 => "minecraft:black_concrete",
+ _ => "minecraft:white_concrete",
+ }
+ .into(),
+ 173 => "minecraft:coal_block".into(),
+ 95 => match meta {
+ 0 => "minecraft:white_stained_glass",
+ 1 => "minecraft:orange_stained_glass",
+ 2 => "minecraft:magenta_stained_glass",
+ 3 => "minecraft:light_blue_stained_glass",
+ 4 => "minecraft:yellow_stained_glass",
+ 5 => "minecraft:lime_stained_glass",
+ 6 => "minecraft:pink_stained_glass",
+ 7 => "minecraft:gray_stained_glass",
+ 8 => "minecraft:light_gray_stained_glass",
+ 9 => "minecraft:cyan_stained_glass",
+ 10 => "minecraft:purple_stained_glass",
+ 11 => "minecraft:blue_stained_glass",
+ 12 => "minecraft:brown_stained_glass",
+ 13 => "minecraft:green_stained_glass",
+ 14 => "minecraft:red_stained_glass",
+ 15 => "minecraft:black_stained_glass",
+ _ => "minecraft:white_stained_glass",
+ }
+ .into(),
+ _ => "minecraft:air".into(),
+ }
+}
+
+fn get_contextual_properties(block_id: u8, meta: u8) -> Option {
+ match block_id {
+ 63 => Some(format!("rotation={}", meta & 0x0F)),
+ 68 => Some(format!(
+ "facing={}",
+ match meta {
+ 2 => "north",
+ 3 => "south",
+ 4 => "west",
+ _ => "east",
+ }
+ )),
+ 50 | 75 | 76 => {
+ if meta >= 1 && meta <= 4 {
+ Some(format!(
+ "facing={}",
+ match meta {
+ 1 => "east",
+ 2 => "west",
+ 3 => "south",
+ _ => "north",
+ }
+ ))
+ } else {
+ None
+ }
+ }
+ _ => None,
+ }
+}
+
+fn is_stair_legacy(id: u8) -> bool {
+ matches!(
+ id,
+ 53 | 67 | 108 | 109 | 114 | 128 | 134 | 135 | 136 | 156 | 163 | 164 | 180
+ )
+}
+
+fn get_stair_facing(meta: u8) -> &'static str {
+ match meta & 0x3 {
+ 0 => "east",
+ 1 => "west",
+ 2 => "south",
+ 3 => "north",
+ _ => "north",
+ }
+}
+
+fn get_relative_side(current_facing: &str, neighbor_facing: &str) -> Option<&'static str> {
+ match current_facing {
+ "north" => {
+ if neighbor_facing == "west" {
+ return Some("left");
+ }
+ if neighbor_facing == "east" {
+ return Some("right");
+ }
+ }
+ "south" => {
+ if neighbor_facing == "east" {
+ return Some("left");
+ }
+ if neighbor_facing == "west" {
+ return Some("right");
+ }
+ }
+ "west" => {
+ if neighbor_facing == "south" {
+ return Some("left");
+ }
+ if neighbor_facing == "north" {
+ return Some("right");
+ }
+ }
+ "east" => {
+ if neighbor_facing == "north" {
+ return Some("left");
+ }
+ if neighbor_facing == "south" {
+ return Some("right");
+ }
+ }
+ _ => {}
+ }
+ None
+}
+
+fn is_fence_legacy(id: u8) -> bool {
+ matches!(id, 85 | 113 | 188 | 189 | 190 | 191 | 192)
+}
+
+fn is_fence_gate_legacy(id: u8) -> bool {
+ matches!(id, 107 | 183 | 184 | 185 | 186 | 187)
+}
+
+fn is_pane_legacy(id: u8) -> bool {
+ matches!(id, 101 | 102 | 160)
+}
+
+fn is_glass_legacy(id: u8) -> bool {
+ matches!(id, 20 | 95)
+}
+
+fn is_likely_solid_attachment(id: u8) -> bool {
+ if id == 0 {
+ return false;
+ }
+ if id >= 8 && id <= 11 {
+ return false;
+ }
+ !matches!(
+ id,
+ 6 | 27 | 28 | 30 | 31 | 32 | 37 | 38 | 39 | 40 | 50 | 51 | 55 | 59 | 63 | 65 | 66
+ | 68 | 69 | 70 | 71 | 72 | 75 | 76 | 77 | 78 | 83 | 90 | 92 | 93 | 94 | 96 | 101
+ | 102 | 104 | 105 | 106 | 107 | 111 | 115 | 119 | 127 | 131 | 132 | 141 | 142 | 143
+ | 147 | 148 | 149 | 150 | 157 | 160 | 171 | 175
+ )
+}
diff --git a/src-tauri/src/commands/java2lce/level_dat.rs b/src-tauri/src/commands/java2lce/level_dat.rs
new file mode 100644
index 0000000..dba4f04
--- /dev/null
+++ b/src-tauri/src/commands/java2lce/level_dat.rs
@@ -0,0 +1,251 @@
+use super::nbt::{self, NbtCompound, NbtValue};
+pub struct SpawnPoint {
+ pub x: i32,
+ pub y: i32,
+ pub z: i32,
+}
+
+pub fn read_spawn(root: &NbtCompound) -> SpawnPoint {
+ let data = match root.compound("Data") {
+ Some(d) => d,
+ None => {
+ return SpawnPoint { x: 0, y: 64, z: 0 };
+ }
+ };
+
+ if let (Some(sx), Some(sz)) = (data.int("SpawnX"), data.int("SpawnZ")) {
+ return SpawnPoint {
+ x: sx,
+ y: data.int("SpawnY").unwrap_or(64),
+ z: sz,
+ };
+ }
+
+ if let Some(spawn_comp) = data.compound("spawn") {
+ if let Some(pos) = spawn_comp.int_array("pos") {
+ if pos.len() >= 3 {
+ return SpawnPoint {
+ x: pos[0],
+ y: pos[1],
+ z: pos[2],
+ };
+ }
+ }
+ }
+
+ SpawnPoint { x: 0, y: 64, z: 0 }
+}
+
+pub fn convert_java_to_lce(
+ java_root: &NbtCompound,
+ spawn_chunk_x: i32,
+ spawn_chunk_z: i32,
+ xz_size: i32,
+ flat_world: bool,
+ override_spawn_y: Option,
+) -> Vec {
+ let java_data = java_root
+ .compound("Data")
+ .expect("Java level.dat missing 'Data' compound tag");
+
+ let data_version = java_data
+ .int("DataVersion")
+ .or_else(|| java_root.int("DataVersion"))
+ .unwrap_or(0);
+ let is_modern_world = data_version >= 1519;
+ let hell_scale: i32 = 3;
+ let spawn = read_spawn(java_root);
+ let spawn_x = spawn.x;
+ let spawn_y = if let Some(oy) = override_spawn_y {
+ oy.clamp(1, 127)
+ } else if is_modern_world {
+ 64
+ } else {
+ spawn.y.clamp(1, 127)
+ };
+ let spawn_z = spawn.z;
+ let new_spawn_x = spawn_x - (spawn_chunk_x * 16);
+ let new_spawn_z = spawn_z - (spawn_chunk_z * 16);
+ let safe_generator_name = if flat_world {
+ "flat".to_string()
+ } else if is_modern_world {
+ "default".to_string()
+ } else {
+ get_string(java_data, "generatorName", "default")
+ };
+ let safe_generator_version = if flat_world {
+ 0
+ } else if is_modern_world {
+ 1
+ } else {
+ get_int(java_data, "generatorVersion")
+ };
+ let safe_generator_options = if flat_world {
+ get_string(java_data, "generatorOptions", "2;7,2x3,2;1;")
+ } else if is_modern_world {
+ "".to_string()
+ } else {
+ get_string(java_data, "generatorOptions", "")
+ };
+
+ let mut lce_data = NbtCompound::new("Data");
+ lce_data.insert("RandomSeed", NbtValue::Long(get_long(java_data, "RandomSeed")));
+ lce_data.insert("generatorName", NbtValue::String(safe_generator_name));
+ lce_data.insert("generatorVersion", NbtValue::Int(safe_generator_version));
+ lce_data.insert("generatorOptions", NbtValue::String(safe_generator_options));
+ lce_data.insert("GameType", NbtValue::Int(get_int(java_data, "GameType")));
+ lce_data.insert("MapFeatures", NbtValue::Byte(get_bool(java_data, "MapFeatures", true)));
+ lce_data.insert("SpawnX", NbtValue::Int(new_spawn_x));
+ lce_data.insert("SpawnY", NbtValue::Int(spawn_y));
+ lce_data.insert("SpawnZ", NbtValue::Int(new_spawn_z));
+ lce_data.insert("Time", NbtValue::Long(get_long(java_data, "Time")));
+ lce_data.insert("DayTime", NbtValue::Long(get_long(java_data, "DayTime")));
+ lce_data.insert("SizeOnDisk", NbtValue::Long(0));
+ lce_data.insert("LastPlayed", NbtValue::Long(unix_timestamp_millis()));
+ lce_data.insert(
+ "LevelName",
+ NbtValue::String(get_string(java_data, "LevelName", "Converted World")),
+ );
+ lce_data.insert("version", NbtValue::Int(19133));
+ lce_data.insert("rainTime", NbtValue::Int(get_int(java_data, "rainTime")));
+ lce_data.insert("raining", NbtValue::Byte(get_bool(java_data, "raining", false)));
+ lce_data.insert("thunderTime", NbtValue::Int(get_int(java_data, "thunderTime")));
+ lce_data.insert(
+ "thundering",
+ NbtValue::Byte(get_bool(java_data, "thundering", false)),
+ );
+ lce_data.insert(
+ "hardcore",
+ NbtValue::Byte(get_bool(java_data, "hardcore", false)),
+ );
+ lce_data.insert(
+ "allowCommands",
+ NbtValue::Byte(get_bool(java_data, "allowCommands", false)),
+ );
+ lce_data.insert(
+ "initialized",
+ NbtValue::Byte(get_bool(java_data, "initialized", true)),
+ );
+
+ lce_data.insert("newSeaLevel", NbtValue::Byte(1));
+ lce_data.insert(
+ "hasBeenInCreative",
+ NbtValue::Byte(get_bool(java_data, "hasBeenInCreative", false)),
+ );
+ lce_data.insert(
+ "spawnBonusChest",
+ NbtValue::Byte(get_bool(java_data, "spawnBonusChest", false)),
+ );
+
+ lce_data.insert("hasStronghold", NbtValue::Byte(0));
+ lce_data.insert("StrongholdX", NbtValue::Int(0));
+ lce_data.insert("StrongholdY", NbtValue::Int(0));
+ lce_data.insert("StrongholdZ", NbtValue::Int(0));
+ lce_data.insert("hasStrongholdEndPortal", NbtValue::Byte(0));
+ lce_data.insert("StrongholdEndPortalX", NbtValue::Int(0));
+ lce_data.insert("StrongholdEndPortalZ", NbtValue::Int(0));
+ lce_data.insert("XZSize", NbtValue::Int(xz_size));
+ lce_data.insert("HellScale", NbtValue::Int(hell_scale));
+ if !is_modern_world {
+ if let Some(game_rules) = java_data.get("GameRules") {
+ lce_data.insert("GameRules", game_rules.clone());
+ }
+ }
+
+ let mut root = NbtCompound::new("");
+ root.insert("Data", NbtValue::Compound(lce_data));
+ nbt::write_nbt(&root)
+}
+
+pub fn convert_lce_to_java(
+ lce_root: &NbtCompound,
+ override_spawn_x: Option,
+ override_spawn_y: Option,
+ override_spawn_z: Option,
+ embedded_player: Option<&NbtCompound>,
+) -> Vec {
+ let lce_data = lce_root.compound("Data").unwrap_or(lce_root);
+ let mut java_data = lce_data.clone();
+ let lce_only_fields = [
+ "newSeaLevel",
+ "hasBeenInCreative",
+ "spawnBonusChest",
+ "XZSize",
+ "xzSize",
+ "HellScale",
+ "hellScale",
+ "hasStronghold",
+ "StrongholdX",
+ "StrongholdY",
+ "StrongholdZ",
+ "hasStrongholdEndPortal",
+ "StrongholdEndPortalX",
+ "StrongholdEndPortalZ",
+ "xStronghold",
+ "yStronghold",
+ "zStronghold",
+ "hasStrongholdEP",
+ "xStrongholdEP",
+ "zStrongholdEP",
+ ];
+
+ for field in lce_only_fields {
+ java_data.remove(field);
+ }
+
+ if let Some(sx) = override_spawn_x {
+ java_data.insert("SpawnX", NbtValue::Int(sx));
+ }
+ if let Some(sy) = override_spawn_y {
+ java_data.insert("SpawnY", NbtValue::Int(sy));
+ }
+ if let Some(sz) = override_spawn_z {
+ java_data.insert("SpawnZ", NbtValue::Int(sz));
+ }
+
+ java_data.insert("version", NbtValue::Int(19133));
+ java_data.insert("DataVersion", NbtValue::Int(1343));
+ let mut version_compound = NbtCompound::new("Version");
+ version_compound.insert("Id", NbtValue::Int(1343));
+ version_compound.insert("Name", NbtValue::String("1.12.2".to_string()));
+ version_compound.insert("Snapshot", NbtValue::Byte(0));
+ java_data.insert("Version", NbtValue::Compound(version_compound));
+ if let Some(player) = embedded_player {
+ let mut player_tag = player.clone();
+ player_tag.name = "Player".to_string();
+ java_data.insert("Player", NbtValue::Compound(player_tag));
+ }
+
+ java_data.insert("LastPlayed", NbtValue::Long(unix_timestamp_millis()));
+ let mut java_root = NbtCompound::new("");
+ let mut data_tag = NbtCompound::new("Data");
+ for (name, value) in &java_data.tags {
+ data_tag.insert(name, value.clone());
+ }
+ java_root.insert("Data", NbtValue::Compound(data_tag));
+
+ nbt::write_gzip_nbt(&java_root)
+}
+
+fn get_long(compound: &NbtCompound, name: &str) -> i64 {
+ compound.long(name).unwrap_or(0)
+}
+
+fn get_int(compound: &NbtCompound, name: &str) -> i32 {
+ compound.int(name).unwrap_or(0)
+}
+
+fn get_string(compound: &NbtCompound, name: &str, default: &str) -> String {
+ compound.string(name).unwrap_or(default).to_string()
+}
+
+fn get_bool(compound: &NbtCompound, name: &str, default: bool) -> i8 {
+ compound.byte(name).unwrap_or(if default { 1 } else { 0 })
+}
+
+fn unix_timestamp_millis() -> i64 {
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap_or_default()
+ .as_millis() as i64
+}
diff --git a/src-tauri/src/commands/java2lce/mod.rs b/src-tauri/src/commands/java2lce/mod.rs
new file mode 100644
index 0000000..3531912
--- /dev/null
+++ b/src-tauri/src/commands/java2lce/mod.rs
@@ -0,0 +1,235 @@
+pub mod block_mapping;
+pub mod chunk;
+pub mod payload;
+pub mod level_dat;
+pub mod nbt;
+pub mod region;
+use std::fs;
+use std::path::Path;
+use tauri::AppHandle;
+use std::collections::HashMap;
+use region::{JavaWorldReader, JavaRegionFileWriter, LceRegionFile, SaveDataContainer};
+use chunk::{convert_chunk_for_save, build_modern_anvil_level};
+use level_dat::{read_spawn, convert_java_to_lce, convert_lce_to_java};
+const REGION_SECT_COUNT: usize = 1024;
+#[tauri::command]
+#[allow(non_snake_case)]
+pub async fn java_to_lce(
+ _app: AppHandle,
+ java_world_path: String,
+ output_ms_path: String,
+) -> Result {
+ let reader = JavaWorldReader::new(&java_world_path);
+ let java_root = reader.read_level_dat().map_err(|e| format!("Failed to read level.dat: {}", e))?;
+ let spawn = read_spawn(&java_root);
+ let spawn_chunk_x = spawn.x >> 4;
+ let spawn_chunk_z = spawn.z >> 4;
+ let region_files = reader.get_region_files("");
+ let total_regions = region_files.len();
+ let mut container = SaveDataContainer::new(0, 0);
+ let level_dat_bytes = convert_java_to_lce(
+ &java_root,
+ spawn_chunk_x,
+ spawn_chunk_z,
+ 864,
+ false,
+ None,
+ );
+ let ld_idx = container.create_file("level.dat");
+ container.write_to_file(ld_idx, &level_dat_bytes);
+ let mut java_reader = JavaWorldReader::new(&java_world_path);
+ let mut chunks_written: usize = 0;
+ let mut errors: Vec = Vec::new();
+ for (ri, &(_, _, ref region_path)) in region_files.iter().enumerate() {
+ let parts: Vec<&str> = region_path.split('.').collect();
+ let region_x: i32 = if parts.len() >= 2 {
+ parts[parts.len() - 3].parse().unwrap_or(0)
+ } else {
+ 0
+ };
+ let region_z: i32 = if parts.len() >= 2 {
+ parts[parts.len() - 2].parse().unwrap_or(0)
+ } else {
+ 0
+ };
+
+ let region_filename = format!("r.{}.{}.mcr", region_x, region_z);
+ let mut lce_region = LceRegionFile::new(®ion_filename);
+ let mut chunk_count = 0;
+ for local_z in 0..32 {
+ for local_x in 0..32 {
+ if !java_reader.has_chunk(region_path, local_x, local_z) {
+ continue;
+ }
+
+ match java_reader.read_chunk_nbt(region_path, local_x, local_z) {
+ Ok(Some(root)) => {
+ let chunk_x = region_x * 32 + local_x;
+ let chunk_z = region_z * 32 + local_z;
+ let payload = convert_chunk_for_save(&root, chunk_x, chunk_z, false);
+ lce_region.write_chunk(local_x, local_z, payload);
+ chunk_count += 1;
+ }
+ Ok(None) => {}
+ Err(e) => {
+ errors.push(format!("r.{}/{}: {}", local_x, local_z, e));
+ }
+ }
+ }
+ }
+
+ if chunk_count > 0 {
+ lce_region.write_to_container(&mut container);
+ chunks_written += chunk_count;
+ }
+
+ if ri % 10 == 0 {
+ eprintln!("java2lce: processed region {}/{}", ri + 1, total_regions);
+ }
+ }
+
+ container.save(&output_ms_path).map_err(|e| format!("Failed to save output: {}", e))?;
+ let mut msg = format!(
+ "Conversion complete!\nChunks: {}\nRegions: {}",
+ chunks_written, total_regions
+ );
+ if !errors.is_empty() {
+ msg.push_str(&format!("\nErrors: {}", errors.len()));
+ for err in errors.iter().take(5) {
+ msg.push_str(&format!("\n {}", err));
+ }
+ if errors.len() > 5 {
+ msg.push_str(&format!("\n ... and {} more", errors.len() - 5));
+ }
+ }
+ eprintln!("{}",msg);
+ Ok(msg)
+}
+
+#[tauri::command]
+#[allow(non_snake_case)]
+pub async fn lce_to_java(
+ _app: AppHandle,
+ input_ms_path: String,
+ java_world_output: String,
+) -> Result {
+ let ms_data = fs::read(&input_ms_path).map_err(|e| format!("Failed to read saveData.ms: {}", e))?;
+ let decompressed = region::decompress_zlib(&ms_data[8..]).map_err(|e| format!("Failed to decompress save data: {}", e))?;
+ let footer_offset = u32::from_le_bytes([
+ decompressed[0], decompressed[1], decompressed[2], decompressed[3],
+ ]) as usize;
+ let entry_count = u32::from_le_bytes([
+ decompressed[4], decompressed[5], decompressed[6], decompressed[7],
+ ]) as usize;
+ let mut files: Vec<(String, Vec)> = Vec::new();
+ for i in 0..entry_count {
+ let base = footer_offset + i * 144;
+ if base + 144 > decompressed.len() {
+ break;
+ }
+
+ let name_bytes = &decompressed[base..base + 128];
+ let name: String = name_bytes
+ .chunks_exact(2)
+ .map(|c| u16::from_le_bytes([c[0], c[1]]))
+ .take_while(|&c| c != 0)
+ .filter_map(|c| char::from_u32(c as u32))
+ .collect();
+
+ let length = u32::from_le_bytes([
+ decompressed[base + 128],
+ decompressed[base + 129],
+ decompressed[base + 130],
+ decompressed[base + 131],
+ ]) as usize;
+ let start_offset = u32::from_le_bytes([
+ decompressed[base + 132],
+ decompressed[base + 133],
+ decompressed[base + 134],
+ decompressed[base + 135],
+ ]) as usize;
+ if !name.is_empty() && length > 0 && start_offset + length <= decompressed.len() {
+ let data = decompressed[start_offset..start_offset + length].to_vec();
+ files.push((name, data));
+ }
+ }
+
+ let out_dir = Path::new(&java_world_output);
+ fs::create_dir_all(out_dir).map_err(|e| format!("Failed to create output directory: {}", e))?;
+ fs::create_dir_all(out_dir.join("region")).map_err(|e| format!("Failed to create region directory: {}", e))?;
+ let mut chunks_written: usize = 0;
+ let mut errors: Vec = Vec::new();
+ let mut region_writers: HashMap = HashMap::new();
+ for (name, data) in &files {
+ if name == "level.dat" {
+ let lce_root = nbt::read_nbt(data).map_err(|e| format!("Failed to parse level.dat: {}", e))?;
+ let java_level_bytes = convert_lce_to_java(&lce_root, None, None, None, None);
+ fs::write(out_dir.join("level.dat"), &java_level_bytes).map_err(|e| format!("Failed to write level.dat: {}", e))?;
+ continue;
+ }
+
+ if name.ends_with(".mcr") || name.ends_with(".mca") {
+ let mut reader = region::JavaRegionReader::open_from_bytes(data);
+ let parts: Vec<&str> = name.split('.').collect();
+ let region_x: i32 = if parts.len() >= 4 { parts[1].parse().unwrap_or(0) } else { 0 };
+ let region_z: i32 = if parts.len() >= 4 { parts[2].parse().unwrap_or(0) } else { 0 };
+ for local_z in 0..32 {
+ for local_x in 0..32 {
+ let index = (local_x & 31) + (local_z & 31) * 32;
+ if reader.offsets[index as usize] == 0 {
+ continue;
+ }
+
+ match reader.read_chunk(local_x, local_z) {
+ Ok(Some(lce_chunk_data)) => {
+ if let Some(legacy_nbt) = payload::try_decode_to_legacy_nbt(&lce_chunk_data) {
+ let root = nbt::read_nbt(&legacy_nbt).unwrap_or_default();
+ let source_level = root.compound("Level").unwrap_or(&root);
+ let cx = region_x * 32 + local_x;
+ let cz = region_z * 32 + local_z;
+ let modern_root = build_modern_anvil_level(source_level, cx, cz);
+ let modern_nbt = nbt::write_nbt(&modern_root);
+ let out_region_x = cx >> 5;
+ let out_region_z = cz >> 5;
+ let out_local_x = cx & 31;
+ let out_local_z = cz & 31;
+ let section_region = format!("r.{}.{}.mca", out_region_x, out_region_z);
+ let region_path = out_dir.join("region").join(§ion_region);
+ let region_path_str = region_path.to_string_lossy().to_string();
+ let writer = region_writers.entry(section_region.clone()).or_insert_with(|| {
+ JavaRegionFileWriter::load_from_file(®ion_path_str)
+ });
+ writer.write_chunk(out_local_x, out_local_z, &modern_nbt);
+ chunks_written += 1;
+ }
+ }
+ Ok(None) => {}
+ Err(e) => {
+ errors.push(format!("{}/{}: {}", name, local_x * 32 + local_z, e));
+ }
+ }
+ }
+ }
+ continue;
+ }
+ }
+
+ for (region_name, writer) in &mut region_writers {
+ if let Err(e) = writer.save() {
+ errors.push(format!("Failed to save region {}: {}", region_name, e));
+ }
+ }
+
+ let mut msg = format!(
+ "Conversion complete!\nChunks: {}\nFiles: {}",
+ chunks_written, files.len()
+ );
+ if !errors.is_empty() {
+ msg.push_str(&format!("\nErrors: {}", errors.len()));
+ for err in errors.iter().take(5) {
+ msg.push_str(&format!("\n {}", err));
+ }
+ }
+ eprintln!("{}", msg);
+ Ok(msg)
+}
diff --git a/src-tauri/src/commands/java2lce/nbt.rs b/src-tauri/src/commands/java2lce/nbt.rs
new file mode 100644
index 0000000..bc0324f
--- /dev/null
+++ b/src-tauri/src/commands/java2lce/nbt.rs
@@ -0,0 +1,496 @@
+use std::io::{Read, Write};
+use flate2::read::GzDecoder;
+use flate2::write::GzEncoder;
+use flate2::Compression;
+#[derive(Debug, Clone, PartialEq)]
+pub enum NbtValue {
+ Byte(i8),
+ Short(i16),
+ Int(i32),
+ Long(i64),
+ Float(f32),
+ Double(f64),
+ ByteArray(Vec),
+ String(String),
+ List(Vec),
+ Compound(NbtCompound),
+ IntArray(Vec),
+ LongArray(Vec),
+}
+
+#[derive(Debug, Clone, Default, PartialEq)]
+pub struct NbtCompound {
+ pub name: String,
+ pub tags: Vec<(String, NbtValue)>,
+}
+
+impl NbtCompound {
+ pub fn new(name: &str) -> Self {
+ Self {
+ name: name.to_string(),
+ tags: Vec::new(),
+ }
+ }
+
+ pub fn insert(&mut self, name: &str, value: NbtValue) {
+ self.tags.retain(|(n, _)| n != name);
+ self.tags.push((name.to_string(), value));
+ }
+
+ pub fn get(&self, name: &str) -> Option<&NbtValue> {
+ self.tags.iter().find(|(n, _)| n == name).map(|(_, v)| v)
+ }
+
+ pub fn get_mut(&mut self, name: &str) -> Option<&mut NbtValue> {
+ self.tags.iter_mut().find(|(n, _)| n == name).map(|(_, v)| v)
+ }
+
+ pub fn contains(&self, name: &str) -> bool {
+ self.tags.iter().any(|(n, _)| n == name)
+ }
+
+ pub fn remove(&mut self, name: &str) {
+ self.tags.retain(|(n, _)| n != name);
+ }
+
+ pub fn compound(&self, name: &str) -> Option<&NbtCompound> {
+ self.get(name).and_then(|v| match v {
+ NbtValue::Compound(c) => Some(c),
+ _ => None,
+ })
+ }
+
+ pub fn compound_mut(&mut self, name: &str) -> Option<&mut NbtCompound> {
+ self.get_mut(name).and_then(|v| match v {
+ NbtValue::Compound(c) => Some(c),
+ _ => None,
+ })
+ }
+
+ pub fn byte(&self, name: &str) -> Option {
+ self.get(name).and_then(|v| match v {
+ NbtValue::Byte(b) => Some(*b),
+ _ => None,
+ })
+ }
+
+ pub fn short(&self, name: &str) -> Option {
+ self.get(name).and_then(|v| match v {
+ NbtValue::Short(s) => Some(*s),
+ _ => None,
+ })
+ }
+
+ pub fn int(&self, name: &str) -> Option {
+ self.get(name).and_then(|v| match v {
+ NbtValue::Int(i) => Some(*i),
+ _ => None,
+ })
+ }
+
+ pub fn long(&self, name: &str) -> Option {
+ self.get(name).and_then(|v| match v {
+ NbtValue::Long(l) => Some(*l),
+ _ => None,
+ })
+ }
+
+ pub fn float(&self, name: &str) -> Option {
+ self.get(name).and_then(|v| match v {
+ NbtValue::Float(f) => Some(*f),
+ _ => None,
+ })
+ }
+
+ pub fn double(&self, name: &str) -> Option {
+ self.get(name).and_then(|v| match v {
+ NbtValue::Double(d) => Some(*d),
+ _ => None,
+ })
+ }
+
+ pub fn string(&self, name: &str) -> Option<&str> {
+ self.get(name).and_then(|v| match v {
+ NbtValue::String(s) => Some(s.as_str()),
+ _ => None,
+ })
+ }
+
+ pub fn byte_array(&self, name: &str) -> Option<&[u8]> {
+ self.get(name).and_then(|v| match v {
+ NbtValue::ByteArray(b) => Some(b.as_slice()),
+ _ => None,
+ })
+ }
+
+ pub fn int_array(&self, name: &str) -> Option<&[i32]> {
+ self.get(name).and_then(|v| match v {
+ NbtValue::IntArray(a) => Some(a.as_slice()),
+ _ => None,
+ })
+ }
+
+ pub fn long_array(&self, name: &str) -> Option<&[i64]> {
+ self.get(name).and_then(|v| match v {
+ NbtValue::LongArray(a) => Some(a.as_slice()),
+ _ => None,
+ })
+ }
+
+ pub fn list(&self, name: &str) -> Option<&[NbtValue]> {
+ self.get(name).and_then(|v| match v {
+ NbtValue::List(l) => Some(l.as_slice()),
+ _ => None,
+ })
+ }
+
+ pub fn list_compounds(&self, name: &str) -> Vec<&NbtCompound> {
+ self.list(name)
+ .unwrap_or(&[])
+ .iter()
+ .filter_map(|v| match v {
+ NbtValue::Compound(c) => Some(c),
+ _ => None,
+ })
+ .collect()
+ }
+}
+
+fn tag_type_id(v: &NbtValue) -> u8 {
+ match v {
+ NbtValue::Byte(_) => 1,
+ NbtValue::Short(_) => 2,
+ NbtValue::Int(_) => 3,
+ NbtValue::Long(_) => 4,
+ NbtValue::Float(_) => 5,
+ NbtValue::Double(_) => 6,
+ NbtValue::ByteArray(_) => 7,
+ NbtValue::String(_) => 8,
+ NbtValue::List(_) => 9,
+ NbtValue::Compound(_) => 10,
+ NbtValue::IntArray(_) => 11,
+ NbtValue::LongArray(_) => 12,
+ }
+}
+
+fn read_be_i16(data: &[u8], off: usize) -> i16 {
+ i16::from_be_bytes([data[off], data[off + 1]])
+}
+
+fn read_be_i32(data: &[u8], off: usize) -> i32 {
+ i32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
+}
+
+fn read_be_i64(data: &[u8], off: usize) -> i64 {
+ i64::from_be_bytes([
+ data[off],
+ data[off + 1],
+ data[off + 2],
+ data[off + 3],
+ data[off + 4],
+ data[off + 5],
+ data[off + 6],
+ data[off + 7],
+ ])
+}
+
+fn read_be_f32(data: &[u8], off: usize) -> f32 {
+ f32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
+}
+
+fn read_be_f64(data: &[u8], off: usize) -> f64 {
+ f64::from_be_bytes([
+ data[off],
+ data[off + 1],
+ data[off + 2],
+ data[off + 3],
+ data[off + 4],
+ data[off + 5],
+ data[off + 6],
+ data[off + 7],
+ ])
+}
+
+fn read_string(data: &[u8], off: usize) -> Result<(String, usize), String> {
+ let len = read_be_i16(data, off) as usize;
+ let start = off + 2;
+ if start + len > data.len() {
+ return Err("string extends past end of data".into());
+ }
+ let s = String::from_utf8_lossy(&data[start..start + len]).to_string();
+ Ok((s, start + len))
+}
+
+pub fn read_nbt(data: &[u8]) -> Result {
+ if data.is_empty() {
+ return Err("empty NBT data".into());
+ }
+ let mut pos = 0;
+ let tag_type = data[pos];
+ pos += 1;
+ if tag_type != 10 {
+ return Err(format!("root tag is not compound (type={})", tag_type));
+ }
+ let (name, new_pos) = read_string(data, pos)?;
+ pos = new_pos;
+ let (compound, _new_pos) = read_compound_payload(data, pos)?;
+ let mut result = compound;
+ result.name = name;
+ Ok(result)
+}
+
+fn read_compound_payload(data: &[u8], mut pos: usize) -> Result<(NbtCompound, usize), String> {
+ let mut compound = NbtCompound::default();
+ loop {
+ if pos >= data.len() {
+ return Err("unexpected end in compound".into());
+ }
+ let tag_type = data[pos];
+ pos += 1;
+ if tag_type == 0 {
+ break;
+ }
+ let (name, new_pos) = read_string(data, pos)?;
+ pos = new_pos;
+ let (value, new_pos) = read_value(data, pos, tag_type)?;
+ pos = new_pos;
+ compound.tags.push((name, value));
+ }
+ Ok((compound, pos))
+}
+
+fn read_value(data: &[u8], pos: usize, tag_type: u8) -> Result<(NbtValue, usize), String> {
+ match tag_type {
+ 1 => Ok((NbtValue::Byte(data[pos] as i8), pos + 1)),
+ 2 => Ok((NbtValue::Short(read_be_i16(data, pos)), pos + 2)),
+ 3 => Ok((NbtValue::Int(read_be_i32(data, pos)), pos + 4)),
+ 4 => Ok((NbtValue::Long(read_be_i64(data, pos)), pos + 8)),
+ 5 => Ok((NbtValue::Float(read_be_f32(data, pos)), pos + 4)),
+ 6 => Ok((NbtValue::Double(read_be_f64(data, pos)), pos + 8)),
+ 7 => {
+ let len = read_be_i32(data, pos) as usize;
+ let start = pos + 4;
+ if start + len > data.len() {
+ return Err("byte array extends past end".into());
+ }
+ Ok((NbtValue::ByteArray(data[start..start + len].to_vec()), start + len))
+ }
+ 8 => {
+ let (s, new_pos) = read_string(data, pos)?;
+ Ok((NbtValue::String(s), new_pos))
+ }
+ 9 => {
+ let elem_type = data[pos];
+ let count = read_be_i32(data, pos + 1) as usize;
+ let mut list_pos = pos + 5;
+ let mut list = Vec::with_capacity(count);
+ for _ in 0..count {
+ let (val, new_pos) = read_value(data, list_pos, elem_type)?;
+ list_pos = new_pos;
+ list.push(val);
+ }
+ Ok((NbtValue::List(list), list_pos))
+ }
+ 10 => {
+ let (compound, new_pos) = read_compound_payload(data, pos)?;
+ Ok((NbtValue::Compound(compound), new_pos))
+ }
+ 11 => {
+ let len = read_be_i32(data, pos) as usize;
+ let start = pos + 4;
+ let mut arr = Vec::with_capacity(len);
+ for i in 0..len {
+ let off = start + i * 4;
+ if off + 4 > data.len() {
+ return Err("int array extends past end".into());
+ }
+ arr.push(read_be_i32(data, off));
+ }
+ Ok((NbtValue::IntArray(arr), start + len * 4))
+ }
+ 12 => {
+ let len = read_be_i32(data, pos) as usize;
+ let start = pos + 4;
+ let mut arr = Vec::with_capacity(len);
+ for i in 0..len {
+ let off = start + i * 8;
+ if off + 8 > data.len() {
+ return Err("long array extends past end".into());
+ }
+ arr.push(read_be_i64(data, off));
+ }
+ Ok((NbtValue::LongArray(arr), start + len * 8))
+ }
+ _ => Err(format!("unknown NBT tag type: {}", tag_type)),
+ }
+}
+
+pub fn write_nbt(compound: &NbtCompound) -> Vec {
+ let mut out = Vec::new();
+ out.push(10);
+ write_string(&mut out, &compound.name);
+ write_compound_payload(&mut out, compound);
+ out.push(0);
+ out
+}
+
+fn write_string(out: &mut Vec, s: &str) {
+ let bytes = s.as_bytes();
+ let len = (bytes.len() as i16).to_be_bytes();
+ out.extend_from_slice(&len);
+ out.extend_from_slice(bytes);
+}
+
+fn write_compound_payload(out: &mut Vec, compound: &NbtCompound) {
+ for (name, value) in &compound.tags {
+ out.push(tag_type_id(value));
+ write_string(out, name);
+ write_value(out, value);
+ }
+}
+
+fn write_value(out: &mut Vec, value: &NbtValue) {
+ match value {
+ NbtValue::Byte(b) => out.push(*b as u8),
+ NbtValue::Short(s) => out.extend_from_slice(&s.to_be_bytes()),
+ NbtValue::Int(i) => out.extend_from_slice(&i.to_be_bytes()),
+ NbtValue::Long(l) => out.extend_from_slice(&l.to_be_bytes()),
+ NbtValue::Float(f) => out.extend_from_slice(&f.to_be_bytes()),
+ NbtValue::Double(d) => out.extend_from_slice(&d.to_be_bytes()),
+ NbtValue::ByteArray(arr) => {
+ out.extend_from_slice(&(arr.len() as i32).to_be_bytes());
+ out.extend_from_slice(arr);
+ }
+ NbtValue::String(s) => write_string(out, s),
+ NbtValue::List(list) => {
+ let elem_type = list.first().map(|v| tag_type_id(v)).unwrap_or(0);
+ out.push(elem_type);
+ out.extend_from_slice(&(list.len() as i32).to_be_bytes());
+ for item in list {
+ write_value(out, item);
+ }
+ }
+ NbtValue::Compound(c) => {
+ write_compound_payload(out, c);
+ out.push(0);
+ }
+ NbtValue::IntArray(arr) => {
+ out.extend_from_slice(&(arr.len() as i32).to_be_bytes());
+ for i in arr {
+ out.extend_from_slice(&i.to_be_bytes());
+ }
+ }
+ NbtValue::LongArray(arr) => {
+ out.extend_from_slice(&(arr.len() as i32).to_be_bytes());
+ for l in arr {
+ out.extend_from_slice(&l.to_be_bytes());
+ }
+ }
+ }
+}
+
+pub fn read_gzip_nbt(data: &[u8]) -> Result {
+ let mut decoder = GzDecoder::new(data);
+ let mut buf = Vec::new();
+ decoder
+ .read_to_end(&mut buf)
+ .map_err(|e| format!("gzip decompress failed: {}", e))?;
+ read_nbt(&buf)
+}
+
+pub fn write_gzip_nbt(compound: &NbtCompound) -> Vec {
+ let raw = write_nbt(compound);
+ let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
+ encoder.write_all(&raw).unwrap();
+ encoder.finish().unwrap()
+}
+
+pub fn read_zlib_nbt(data: &[u8]) -> Result {
+ use flate2::read::ZlibDecoder;
+ let mut decoder = ZlibDecoder::new(data);
+ let mut buf = Vec::new();
+ decoder
+ .read_to_end(&mut buf)
+ .map_err(|e| format!("zlib decompress failed: {}", e))?;
+ read_nbt(&buf)
+}
+
+pub fn write_zlib_nbt(compound: &NbtCompound) -> Vec {
+ use flate2::write::ZlibEncoder;
+ let raw = write_nbt(compound);
+ let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
+ encoder.write_all(&raw).unwrap();
+ encoder.finish().unwrap()
+}
+
+pub fn get_byte_array_or(compound: &NbtCompound, name: &str, default_len: usize) -> Vec {
+ compound
+ .byte_array(name)
+ .map(|b| {
+ let mut v = b.to_vec();
+ if v.len() < default_len {
+ v.resize(default_len, 0);
+ }
+ v
+ })
+ .unwrap_or_else(|| vec![0u8; default_len])
+}
+
+pub fn get_int_array_or(compound: &NbtCompound, name: &str, default_len: usize) -> Vec {
+ compound
+ .int_array(name)
+ .map(|a| {
+ let mut v = a.to_vec();
+ if v.len() < default_len {
+ v.resize(default_len, 0);
+ }
+ v
+ })
+ .unwrap_or_else(|| vec![0i32; default_len])
+}
+
+pub fn get_long_array_or(compound: &NbtCompound, name: &str, default_len: usize) -> Vec {
+ compound
+ .long_array(name)
+ .map(|a| {
+ let mut v = a.to_vec();
+ if v.len() < default_len {
+ v.resize(default_len, 0);
+ }
+ v
+ })
+ .unwrap_or_else(|| vec![0i64; default_len])
+}
+
+pub fn get_nibble(data: &[u8], index: usize) -> u8 {
+ let byte_index = index >> 1;
+ if byte_index >= data.len() {
+ return 0;
+ }
+ let b = data[byte_index];
+ if index & 1 == 0 {
+ b & 0x0F
+ } else {
+ (b >> 4) & 0x0F
+ }
+}
+
+pub fn set_nibble(data: &mut [u8], index: usize, value: u8) {
+ let byte_index = index >> 1;
+ if byte_index >= data.len() {
+ return;
+ }
+ let val = value & 0x0F;
+ if index & 1 == 0 {
+ data[byte_index] = (data[byte_index] & 0xF0) | val;
+ } else {
+ data[byte_index] = (data[byte_index] & 0x0F) | (val << 4);
+ }
+}
+
+pub fn clone_or_empty_list(compound: &NbtCompound, name: &str) -> Vec {
+ compound
+ .list(name)
+ .map(|l| l.to_vec())
+ .unwrap_or_default()
+}
diff --git a/src-tauri/src/commands/java2lce/payload.rs b/src-tauri/src/commands/java2lce/payload.rs
new file mode 100644
index 0000000..5238467
--- /dev/null
+++ b/src-tauri/src/commands/java2lce/payload.rs
@@ -0,0 +1,683 @@
+use super::nbt;
+const SAVE_FILE_VERSION_COMPRESSED_CHUNK_STORAGE: i16 = 8;
+const SAVE_FILE_VERSION_CHUNK_INHABITED_TIME: i16 = 9;
+const COMPRESSED_CHUNK_SECTION_HEIGHT: usize = 128;
+const BLOCKS_PER_SECTION: usize = COMPRESSED_CHUNK_SECTION_HEIGHT * 16 * 16;
+const NIBBLES_PER_SECTION: usize = BLOCKS_PER_SECTION / 2;
+const FULL_CHUNK_BLOCKS: usize = 256 * 16 * 16;
+const FULL_CHUNK_NIBBLES: usize = FULL_CHUNK_BLOCKS / 2;
+const INDEX_TYPE_MASK: u16 = 0x0003;
+const INDEX_TYPE_1BIT: u16 = 0x0000;
+const INDEX_TYPE_2BIT: u16 = 0x0001;
+const INDEX_TYPE_4BIT: u16 = 0x0002;
+const INDEX_TYPE_0_OR_8BIT: u16 = 0x0003;
+const INDEX_TYPE_0BIT_FLAG: u16 = 0x0004;
+const SPARSE_ALL_ZERO_INDEX: u8 = 128;
+const SPARSE_ALL_FIFTEEN_INDEX: u8 = 129;
+fn read_be_i16(data: &[u8], off: usize) -> i16 {
+ i16::from_be_bytes([data[off], data[off + 1]])
+}
+
+fn read_be_i32(data: &[u8], off: usize) -> i32 {
+ i32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
+}
+
+fn read_be_i64(data: &[u8], off: usize) -> i64 {
+ i64::from_be_bytes([
+ data[off], data[off + 1], data[off + 2], data[off + 3], data[off + 4], data[off + 5],
+ data[off + 6], data[off + 7],
+ ])
+}
+
+fn write_be_i16(out: &mut Vec, v: i16) {
+ out.extend_from_slice(&v.to_be_bytes());
+}
+
+fn write_be_i32(out: &mut Vec, v: i32) {
+ out.extend_from_slice(&v.to_be_bytes());
+}
+
+fn write_be_i64(out: &mut Vec, v: i64) {
+ out.extend_from_slice(&v.to_be_bytes());
+}
+
+fn get_compressed_tile_index(block: usize, tile: usize) -> usize {
+ let mut index = ((block & 0x180) << 6) | ((block & 0x060) << 4) | ((block & 0x01F) << 2);
+ index |= ((tile & 0x30) << 7) | ((tile & 0x0C) << 5) | (tile & 0x03);
+ index
+}
+
+fn get_nibble_value(nibble_data: &[u8], xz: usize, y: usize) -> u8 {
+ let pos = (xz << 7) | y;
+ let slot = pos >> 1;
+ let part = pos & 1;
+ if slot >= nibble_data.len() {
+ return 0;
+ }
+ let b = nibble_data[slot];
+ if part == 0 {
+ b & 0x0F
+ } else {
+ (b >> 4) & 0x0F
+ }
+}
+
+fn set_nibble_value(nibble_data: &mut [u8], xz: usize, y: usize, value: u8) {
+ let pos = (xz << 7) | y;
+ let slot = pos >> 1;
+ let part = pos & 1;
+ if slot >= nibble_data.len() {
+ return;
+ }
+ let val = value & 0x0F;
+ if part == 0 {
+ nibble_data[slot] = (nibble_data[slot] & 0xF0) | val;
+ } else {
+ nibble_data[slot] = (nibble_data[slot] & 0x0F) | (val << 4);
+ }
+}
+
+fn ensure_length(input: &[u8], expected_len: usize) -> Vec {
+ if input.len() == expected_len {
+ return input.to_vec();
+ }
+ let mut v = vec![0u8; expected_len];
+ let copy_len = input.len().min(expected_len);
+ v[..copy_len].copy_from_slice(&input[..copy_len]);
+ v
+}
+
+fn extract_lower_block_section(full_blocks: &[u8]) -> Vec {
+ if full_blocks.len() == BLOCKS_PER_SECTION {
+ return full_blocks.to_vec();
+ }
+ let mut lower = vec![0u8; BLOCKS_PER_SECTION];
+ for xz in 0..256 {
+ let src_start = xz * 256;
+ let dst_start = xz * COMPRESSED_CHUNK_SECTION_HEIGHT;
+ let copy_len = COMPRESSED_CHUNK_SECTION_HEIGHT.min(full_blocks.len() - src_start.min(full_blocks.len()));
+ if src_start + copy_len <= full_blocks.len() && dst_start + copy_len <= BLOCKS_PER_SECTION {
+ lower[dst_start..dst_start + copy_len]
+ .copy_from_slice(&full_blocks[src_start..src_start + copy_len]);
+ }
+ }
+ lower
+}
+
+fn extract_upper_block_section(full_blocks: &[u8]) -> Vec {
+ if full_blocks.len() < FULL_CHUNK_BLOCKS {
+ return vec![0u8; BLOCKS_PER_SECTION];
+ }
+ let mut upper = vec![0u8; BLOCKS_PER_SECTION];
+ for xz in 0..256 {
+ let src_start = xz * 256 + COMPRESSED_CHUNK_SECTION_HEIGHT;
+ let dst_start = xz * COMPRESSED_CHUNK_SECTION_HEIGHT;
+ upper[dst_start..dst_start + COMPRESSED_CHUNK_SECTION_HEIGHT].copy_from_slice(&full_blocks[src_start..src_start + COMPRESSED_CHUNK_SECTION_HEIGHT]);
+ }
+ upper
+}
+
+fn extract_lower_nibble_section(full_nibbles: &[u8]) -> Vec {
+ let nibble_height = COMPRESSED_CHUNK_SECTION_HEIGHT / 2;
+ if full_nibbles.len() == NIBBLES_PER_SECTION {
+ return full_nibbles.to_vec();
+ }
+ let mut lower = vec![0u8; NIBBLES_PER_SECTION];
+ for xz in 0..256 {
+ let src_start = xz * nibble_height * 2;
+ let dst_start = xz * nibble_height;
+ let copy_len = nibble_height.min(
+ full_nibbles.len().saturating_sub(src_start),
+ );
+ if copy_len > 0 {
+ lower[dst_start..dst_start + copy_len].copy_from_slice(&full_nibbles[src_start..src_start + copy_len]);
+ }
+ }
+ lower
+}
+
+fn extract_upper_nibble_section(full_nibbles: &[u8]) -> Vec {
+ let nibble_height = COMPRESSED_CHUNK_SECTION_HEIGHT / 2;
+ if full_nibbles.len() < FULL_CHUNK_NIBBLES {
+ return vec![0u8; NIBBLES_PER_SECTION];
+ }
+ let mut upper = vec![0u8; NIBBLES_PER_SECTION];
+ for xz in 0..256 {
+ let src_start = xz * nibble_height * 2 + nibble_height;
+ let dst_start = xz * nibble_height;
+ upper[dst_start..dst_start + nibble_height]
+ .copy_from_slice(&full_nibbles[src_start..src_start + nibble_height]);
+ }
+ upper
+}
+
+fn combine_block_sections(lower: &[u8], upper: &[u8]) -> Vec {
+ if lower.len() == FULL_CHUNK_BLOCKS {
+ return lower.to_vec();
+ }
+ let mut combined = vec![0u8; FULL_CHUNK_BLOCKS];
+ for xz in 0..256 {
+ let lower_start = xz * COMPRESSED_CHUNK_SECTION_HEIGHT;
+ let out_start = xz * 256;
+ let copy_len = COMPRESSED_CHUNK_SECTION_HEIGHT.min(lower.len().saturating_sub(lower_start));
+ if copy_len > 0 {
+ combined[out_start..out_start + copy_len].copy_from_slice(&lower[lower_start..lower_start + copy_len]);
+ }
+ let upper_start = xz * COMPRESSED_CHUNK_SECTION_HEIGHT;
+ let out_upper = out_start + COMPRESSED_CHUNK_SECTION_HEIGHT;
+ let copy_len2 = COMPRESSED_CHUNK_SECTION_HEIGHT.min(upper.len().saturating_sub(upper_start));
+ if copy_len2 > 0 {
+ combined[out_upper..out_upper + copy_len2]
+ .copy_from_slice(&upper[upper_start..upper_start + copy_len2]);
+ }
+ }
+ combined
+}
+
+fn combine_nibble_sections(lower: &[u8], upper: &[u8]) -> Vec {
+ let nibble_height = COMPRESSED_CHUNK_SECTION_HEIGHT / 2;
+ if lower.len() == FULL_CHUNK_NIBBLES {
+ return lower.to_vec();
+ }
+ let mut combined = vec![0u8; FULL_CHUNK_NIBBLES];
+ for xz in 0..256 {
+ let lower_start = xz * nibble_height;
+ let out_start = xz * nibble_height * 2;
+ let copy_len = nibble_height.min(lower.len().saturating_sub(lower_start));
+ if copy_len > 0 {
+ combined[out_start..out_start + copy_len]
+ .copy_from_slice(&lower[lower_start..lower_start + copy_len]);
+ }
+ let upper_start = xz * nibble_height;
+ let out_upper = out_start + nibble_height;
+ let copy_len2 = nibble_height.min(upper.len().saturating_sub(upper_start));
+ if copy_len2 > 0 {
+ combined[out_upper..out_upper + copy_len2]
+ .copy_from_slice(&upper[upper_start..upper_start + copy_len2]);
+ }
+ }
+ combined
+}
+
+pub fn encode_legacy_nbt(level: &nbt::NbtCompound) -> Vec {
+ let mut root = nbt::NbtCompound::new("");
+ root.insert("Level", nbt::NbtValue::Compound(level.clone()));
+ nbt::write_nbt(&root)
+}
+
+fn write_compressed_tile_storage(out: &mut Vec, blocks: &[u8]) {
+ let normalized = ensure_length(blocks, BLOCKS_PER_SECTION);
+ let mut blob = vec![0u8; 1024 + BLOCKS_PER_SECTION];
+ let mut data_offset: usize = 0;
+ for block in 0..512 {
+ let index = (INDEX_TYPE_0_OR_8BIT | ((data_offset as u16) << 1)) as u16;
+ blob[block * 2..block * 2 + 2].copy_from_slice(&index.to_le_bytes());
+ for tile in 0..64 {
+ blob[1024 + data_offset + tile] = normalized[get_compressed_tile_index(block, tile)];
+ }
+ data_offset += 64;
+ }
+
+ write_be_i32(out, blob.len() as i32);
+ out.extend_from_slice(&blob);
+}
+
+fn write_empty_compressed_tile_storage(out: &mut Vec) {
+ let mut blob = vec![0u8; 1024];
+ for block in 0..512 {
+ let index = (INDEX_TYPE_0_OR_8BIT | INDEX_TYPE_0BIT_FLAG) as u16;
+ blob[block * 2..block * 2 + 2].copy_from_slice(&index.to_le_bytes());
+ }
+ write_be_i32(out, blob.len() as i32);
+ out.extend_from_slice(&blob);
+}
+
+fn write_sparse_nibble_storage(out: &mut Vec, nibble_data: &[u8], supports_all_fifteen: bool) {
+ let normalized = ensure_length(nibble_data, NIBBLES_PER_SECTION);
+ let mut plane_indices = [0u8; 128];
+ let mut planes: Vec> = Vec::new();
+ for y in 0..128 {
+ let mut all_zero = true;
+ let mut all_fifteen = supports_all_fifteen;
+ let mut plane = vec![0u8; 128];
+ let mut plane_cursor = 0;
+ for xz in 0..128 {
+ let first = get_nibble_value(&normalized, xz * 2, y);
+ let second = get_nibble_value(&normalized, xz * 2 + 1, y);
+
+ if first != 0 || second != 0 {
+ all_zero = false;
+ }
+ if supports_all_fifteen && (first != 15 || second != 15) {
+ all_fifteen = false;
+ }
+
+ plane[plane_cursor] = first | (second << 4);
+ plane_cursor += 1;
+ }
+
+ if all_zero {
+ plane_indices[y] = SPARSE_ALL_ZERO_INDEX;
+ continue;
+ }
+ if supports_all_fifteen && all_fifteen {
+ plane_indices[y] = SPARSE_ALL_FIFTEEN_INDEX;
+ continue;
+ }
+
+ plane_indices[y] = planes.len() as u8;
+ planes.push(plane);
+ }
+
+ write_be_i32(out, planes.len() as i32);
+ out.extend_from_slice(&plane_indices);
+ for plane in &planes {
+ out.extend_from_slice(plane);
+ }
+}
+
+fn write_empty_sparse_nibble_storage(out: &mut Vec, supports_all_fifteen: bool, fill_with_fifteen: bool) {
+ write_be_i32(out, 0);
+ let fill = if supports_all_fifteen && fill_with_fifteen {
+ SPARSE_ALL_FIFTEEN_INDEX
+ } else {
+ SPARSE_ALL_ZERO_INDEX
+ };
+ let plane_indices = [fill; 128];
+ out.extend_from_slice(&plane_indices);
+}
+
+pub fn encode_compressed_storage(level: &nbt::NbtCompound) -> Vec {
+ let blocks = level.byte_array("Blocks").unwrap_or(&[]);
+ let data = level.byte_array("Data").unwrap_or(&[]);
+ let sky_light_raw = level.byte_array("SkyLight").unwrap_or(&[]);
+ let block_light_raw = level.byte_array("BlockLight").unwrap_or(&[]);
+ let height_map = level.byte_array("HeightMap").unwrap_or(&[]);
+ let biomes = level.byte_array("Biomes").unwrap_or(&[]);
+ let default_sky = vec![0xFF; NIBBLES_PER_SECTION];
+ let default_light = vec![0u8; NIBBLES_PER_SECTION];
+ let sky_light = if sky_light_raw.is_empty() { default_sky.as_slice() } else { sky_light_raw };
+ let block_light = if block_light_raw.is_empty() { default_light.as_slice() } else { block_light_raw };
+ let mut out = Vec::new();
+ write_be_i16(&mut out, SAVE_FILE_VERSION_CHUNK_INHABITED_TIME);
+ write_be_i32(&mut out, level.int("xPos").unwrap_or(0));
+ write_be_i32(&mut out, level.int("zPos").unwrap_or(0));
+ write_be_i64(&mut out, level.long("LastUpdate").unwrap_or(0));
+ write_be_i64(&mut out, level.long("InhabitedTime").unwrap_or(0));
+ write_compressed_tile_storage(&mut out, &extract_lower_block_section(blocks));
+ write_compressed_tile_storage(&mut out, &extract_upper_block_section(blocks));
+ write_sparse_nibble_storage(&mut out, &extract_lower_nibble_section(data), false);
+ write_sparse_nibble_storage(&mut out, &extract_upper_nibble_section(data), false);
+ write_sparse_nibble_storage(&mut out, &extract_lower_nibble_section(sky_light), true);
+ write_sparse_nibble_storage(&mut out, &extract_upper_nibble_section(sky_light), true);
+ write_sparse_nibble_storage(&mut out, &extract_lower_nibble_section(block_light), true);
+ write_sparse_nibble_storage(&mut out, &extract_upper_nibble_section(block_light), true);
+ let hm = ensure_length(height_map, 256);
+ out.extend_from_slice(&hm[..256]);
+ write_be_i16(&mut out, level.short("TerrainPopulatedFlags").unwrap_or(0));
+ let bio = ensure_length(biomes, 256);
+ out.extend_from_slice(&bio[..256]);
+ let dynamic_root = nbt::NbtCompound::new("");
+ let dynamic_bytes = nbt::write_nbt(&dynamic_root);
+ out.extend_from_slice(&dynamic_bytes);
+ out
+}
+
+fn is_compressed_chunk_storage(data: &[u8]) -> bool {
+ if data.len() < 2 + 4 + 4 + 8 {
+ return false;
+ }
+ let version = read_be_i16(data, 0);
+ version == SAVE_FILE_VERSION_COMPRESSED_CHUNK_STORAGE || version == SAVE_FILE_VERSION_CHUNK_INHABITED_TIME
+}
+
+pub fn try_read_chunk_coordinates(
+ data: &[u8],
+) -> Option<(i32, i32, bool)> {
+ if let Some((cx, cz, wrapped)) = try_read_legacy_level_coords(data) {
+ return Some((cx, cz, wrapped));
+ }
+
+ if is_compressed_chunk_storage(data) {
+ let cx = read_be_i32(data, 2);
+ let cz = read_be_i32(data, 6);
+ return Some((cx, cz, false));
+ }
+
+ None
+}
+
+fn try_read_legacy_level_coords(data: &[u8]) -> Option<(i32, i32, bool)> {
+ if data.is_empty() || data[0] != 10 {
+ return None;
+ }
+ let compound = nbt::read_nbt(data).ok()?;
+ let (level, has_wrapper) = if let Some(nbt::NbtValue::Compound(c)) = compound.get("Level") {
+ (c, true)
+ } else {
+ (&compound, false)
+ };
+ let cx = level.int("xPos")?;
+ let cz = level.int("zPos")?;
+ Some((cx, cz, has_wrapper))
+}
+
+pub fn force_chunk_coordinates(data: &[u8], expected_x: i32, expected_z: i32) -> Vec {
+ if data.is_empty() {
+ return Vec::new();
+ }
+
+ if let Ok((mut level, _)) = read_legacy_level(data) {
+ level.insert("xPos", nbt::NbtValue::Int(expected_x));
+ level.insert("zPos", nbt::NbtValue::Int(expected_z));
+ return encode_legacy_nbt(&level);
+ }
+
+ if is_compressed_chunk_storage(data) {
+ let mut patched = data.to_vec();
+ patched[2..6].copy_from_slice(&expected_x.to_be_bytes());
+ patched[6..10].copy_from_slice(&expected_z.to_be_bytes());
+ return patched;
+ }
+
+ Vec::new()
+}
+
+fn read_compressed_tile_storage(data: &[u8], offset: &mut usize) -> Result, String> {
+ let allocated_size = read_be_i32(data, *offset) as usize;
+ *offset += 4;
+ if allocated_size < 1024 || *offset + allocated_size > data.len() {
+ return Err("Invalid CompressedTileStorage payload.".into());
+ }
+
+ let blob = &data[*offset..*offset + allocated_size];
+ *offset += allocated_size;
+ let data_region = &blob[1024..];
+ let mut blocks = vec![0u8; BLOCKS_PER_SECTION];
+ for block in 0..512 {
+ let block_index = u16::from_le_bytes([blob[block * 2], blob[block * 2 + 1]]);
+ let index_type = block_index & INDEX_TYPE_MASK;
+ if index_type == INDEX_TYPE_0_OR_8BIT {
+ if (block_index & INDEX_TYPE_0BIT_FLAG) != 0 {
+ let value = ((block_index >> 8) & 0xFF) as u8;
+ for tile in 0..64 {
+ blocks[get_compressed_tile_index(block, tile)] = value;
+ }
+ } else {
+ let data_offset = ((block_index >> 1) & 0x7FFE) as usize;
+ if data_offset + 64 > data_region.len() {
+ return Err("Invalid 8-bit CompressedTileStorage offset.".into());
+ }
+ for tile in 0..64 {
+ blocks[get_compressed_tile_index(block, tile)] = data_region[data_offset + tile];
+ }
+ }
+ continue;
+ }
+
+ let bits_per_tile = match index_type {
+ INDEX_TYPE_1BIT => 1,
+ INDEX_TYPE_2BIT => 2,
+ INDEX_TYPE_4BIT => 4,
+ _ => return Err("Unsupported CompressedTileStorage index type.".into()),
+ };
+
+ let tile_type_count = 1usize << bits_per_tile;
+ let tile_type_mask = (tile_type_count - 1) as u8;
+ let index_shift = 3 - index_type as usize;
+ let index_mask_bits = (7 >> index_type) as usize;
+ let index_mask_bytes = (62 >> index_shift) as usize;
+ let packed_data_size = (8usize << index_type) as usize;
+ let data_offset_packed = ((block_index >> 1) & 0x7FFE) as usize;
+ if data_offset_packed + tile_type_count + packed_data_size > data_region.len() {
+ return Err("Invalid packed CompressedTileStorage offset.".into());
+ }
+
+ let tile_types = &data_region[data_offset_packed..data_offset_packed + tile_type_count];
+ let packed = &data_region[data_offset_packed + tile_type_count..data_offset_packed + tile_type_count + packed_data_size];
+ for tile in 0..64 {
+ let idx = (tile >> index_shift) & index_mask_bytes;
+ let bit = (tile & index_mask_bits) * bits_per_tile;
+ let palette_index = (packed[idx] >> bit) & tile_type_mask;
+ blocks[get_compressed_tile_index(block, tile)] = tile_types[palette_index as usize];
+ }
+ }
+
+ Ok(blocks)
+}
+
+fn skip_compressed_tile_storage(data: &[u8], offset: &mut usize) -> Result<(), String> {
+ let allocated_size = read_be_i32(data, *offset) as usize;
+ *offset += 4;
+ if *offset + allocated_size > data.len() {
+ return Err("Invalid CompressedTileStorage payload.".into());
+ }
+ *offset += allocated_size;
+ Ok(())
+}
+
+fn read_sparse_nibble_storage(data: &[u8], offset: &mut usize, supports_all_fifteen: bool) -> Result, String> {
+ let count = read_be_i32(data, *offset) as usize;
+ *offset += 4;
+ let storage_bytes = 128 + count * 128;
+ if count > data.len() || *offset + storage_bytes > data.len() {
+ return Err("Invalid SparseStorage payload.".into());
+ }
+
+ let blob = &data[*offset..*offset + storage_bytes];
+ *offset += storage_bytes;
+ let plane_indices = &blob[..128];
+ let plane_data = &blob[128..];
+ let mut nibble_data = vec![0u8; NIBBLES_PER_SECTION];
+ for y in 0..128 {
+ let plane_index = plane_indices[y];
+ if plane_index == SPARSE_ALL_ZERO_INDEX {
+ continue;
+ }
+ if supports_all_fifteen && plane_index == SPARSE_ALL_FIFTEEN_INDEX {
+ for xz in 0..256 {
+ set_nibble_value(&mut nibble_data, xz, y, 15);
+ }
+ continue;
+ }
+
+ let plane_offset = plane_index as usize * 128;
+ if plane_offset + 128 > plane_data.len() {
+ return Err("Invalid sparse plane index.".into());
+ }
+
+ let plane = &plane_data[plane_offset..plane_offset + 128];
+ for xz in 0..128 {
+ let packed = plane[xz];
+ set_nibble_value(&mut nibble_data, xz * 2, y, packed & 0x0F);
+ set_nibble_value(&mut nibble_data, xz * 2 + 1, y, (packed >> 4) & 0x0F);
+ }
+ }
+
+ Ok(nibble_data)
+}
+
+fn skip_sparse_nibble_storage(data: &[u8], offset: &mut usize) -> Result<(), String> {
+ let count = read_be_i32(data, *offset) as usize;
+ *offset += 4;
+ let storage_bytes = 128 + count * 128;
+ if count > data.len() || *offset + storage_bytes > data.len() {
+ return Err("Invalid SparseStorage payload.".into());
+ }
+ *offset += storage_bytes;
+ Ok(())
+}
+
+fn read_sized_bytes(data: &[u8], offset: &mut usize, length: usize) -> Vec {
+ let end = (*offset + length).min(data.len());
+ let result = data[*offset..end].to_vec();
+ *offset += length;
+ result
+}
+
+fn read_legacy_level(data: &[u8]) -> Result<(nbt::NbtCompound, bool), String> {
+ if data.is_empty() || data[0] != 10 {
+ return Err("Not NBT compound".into());
+ }
+ let file = nbt::read_nbt(data)?;
+ let has_level = file.get("Level").is_some();
+ let level = if let Some(nbt::NbtValue::Compound(c)) = file.get("Level") {
+ let mut cloned = c.clone();
+ cloned.name = "Level".to_string();
+ cloned
+ } else {
+ let mut cloned = file.clone();
+ cloned.name = "Level".to_string();
+ cloned
+ };
+ Ok((level, has_level))
+}
+
+pub fn try_decode_to_legacy_nbt(data: &[u8]) -> Option> {
+ if let Ok((mut level, _)) = read_legacy_level(data) {
+ level.name = "Level".to_string();
+ return Some(encode_legacy_nbt(&level));
+ }
+
+ if !is_compressed_chunk_storage(data) {
+ return None;
+ }
+
+ decode_compressed_chunk_to_legacy_root(data).ok().map(|root| nbt::write_nbt(&root))
+}
+
+fn decode_compressed_chunk_to_legacy_root(data: &[u8]) -> Result {
+ let mut offset = 0;
+ let version = read_be_i16(data, offset);
+ offset = 2;
+ if version != SAVE_FILE_VERSION_COMPRESSED_CHUNK_STORAGE
+ && version != SAVE_FILE_VERSION_CHUNK_INHABITED_TIME
+ {
+ return Err(format!("Unsupported compressed chunk version: {}", version));
+ }
+
+ let chunk_x = read_be_i32(data, offset);
+ offset += 4;
+ let chunk_z = read_be_i32(data, offset);
+ offset += 4;
+ let last_update = read_be_i64(data, offset);
+ offset += 8;
+ let inhabited_time = if version >= SAVE_FILE_VERSION_CHUNK_INHABITED_TIME {
+ let v = read_be_i64(data, offset);
+ offset += 8;
+ v
+ } else {
+ 0
+ };
+
+ let lower_blocks = read_compressed_tile_storage(data, &mut offset)?;
+ let upper_blocks = read_compressed_tile_storage(data, &mut offset)?;
+ let lower_data = read_sparse_nibble_storage(data, &mut offset, false)?;
+ let upper_data = read_sparse_nibble_storage(data, &mut offset, false)?;
+ let lower_sky = read_sparse_nibble_storage(data, &mut offset, true)?;
+ let upper_sky = read_sparse_nibble_storage(data, &mut offset, true)?;
+ let lower_block_light = read_sparse_nibble_storage(data, &mut offset, true)?;
+ let upper_block_light = read_sparse_nibble_storage(data, &mut offset, true)?;
+ let height_map = read_sized_bytes(data, &mut offset, 256);
+ let terrain_populated_flags = read_be_i16(data, offset);
+ offset += 2;
+ let biomes = read_sized_bytes(data, &mut offset, 256);
+ let dynamic_root = if offset < data.len() {
+ nbt::read_nbt(&data[offset..]).unwrap_or_default()
+ } else {
+ nbt::NbtCompound::default()
+ };
+
+ let mut level = nbt::NbtCompound::new("Level");
+ level.insert("xPos", nbt::NbtValue::Int(chunk_x));
+ level.insert("zPos", nbt::NbtValue::Int(chunk_z));
+ level.insert("LastUpdate", nbt::NbtValue::Long(last_update));
+ level.insert("InhabitedTime", nbt::NbtValue::Long(inhabited_time));
+ level.insert(
+ "Blocks",
+ nbt::NbtValue::ByteArray(combine_block_sections(&lower_blocks, &upper_blocks)),
+ );
+ level.insert(
+ "Data",
+ nbt::NbtValue::ByteArray(combine_nibble_sections(&lower_data, &upper_data)),
+ );
+ level.insert(
+ "SkyLight",
+ nbt::NbtValue::ByteArray(combine_nibble_sections(&lower_sky, &upper_sky)),
+ );
+ level.insert(
+ "BlockLight",
+ nbt::NbtValue::ByteArray(combine_nibble_sections(
+ &lower_block_light,
+ &upper_block_light,
+ )),
+ );
+ level.insert("HeightMap", nbt::NbtValue::ByteArray(height_map));
+ level.insert(
+ "TerrainPopulatedFlags",
+ nbt::NbtValue::Short(terrain_populated_flags),
+ );
+ level.insert("Biomes", nbt::NbtValue::ByteArray(biomes));
+ let entities = dynamic_root
+ .list("Entities")
+ .map(|l| nbt::NbtValue::List(l.to_vec()))
+ .unwrap_or_else(|| nbt::NbtValue::List(Vec::new()));
+ level.insert("Entities", entities);
+ let tile_entities = dynamic_root
+ .list("TileEntities")
+ .map(|l| nbt::NbtValue::List(l.to_vec()))
+ .unwrap_or_else(|| nbt::NbtValue::List(Vec::new()));
+ level.insert("TileEntities", tile_entities);
+ if let Some(tile_ticks) = dynamic_root.get("TileTicks") {
+ level.insert("TileTicks", tile_ticks.clone());
+ }
+
+ let mut root = nbt::NbtCompound::new("");
+ root.insert("Level", nbt::NbtValue::Compound(level));
+ Ok(root)
+}
+
+pub fn try_get_compressed_chunk_nbt_offset(data: &[u8]) -> Option {
+ if !is_compressed_chunk_storage(data) {
+ return None;
+ }
+
+ let mut offset = 0;
+ let version = read_be_i16(data, offset);
+ offset = 2;
+ if version != SAVE_FILE_VERSION_COMPRESSED_CHUNK_STORAGE
+ && version != SAVE_FILE_VERSION_CHUNK_INHABITED_TIME
+ {
+ return None;
+ }
+
+ offset += 8;
+ if version >= SAVE_FILE_VERSION_CHUNK_INHABITED_TIME {
+ offset += 8;
+ }
+
+ let r1 = skip_compressed_tile_storage(data, &mut offset);
+ if r1.is_err() { return None; }
+ let r2 = skip_compressed_tile_storage(data, &mut offset);
+ if r2.is_err() { return None; }
+ let r3 = skip_sparse_nibble_storage(data, &mut offset);
+ if r3.is_err() { return None; }
+ let r4 = skip_sparse_nibble_storage(data, &mut offset);
+ if r4.is_err() { return None; }
+ let r5 = skip_sparse_nibble_storage(data, &mut offset);
+ if r5.is_err() { return None; }
+ let r6 = skip_sparse_nibble_storage(data, &mut offset);
+ if r6.is_err() { return None; }
+ let r7 = skip_sparse_nibble_storage(data, &mut offset);
+ if r7.is_err() { return None; }
+ let r8 = skip_sparse_nibble_storage(data, &mut offset);
+ if r8.is_err() { return None; }
+ offset += 256;
+ offset += 2;
+ offset += 256;
+ if offset > data.len() {
+ return None;
+ }
+
+ Some(offset)
+}
diff --git a/src-tauri/src/commands/java2lce/region.rs b/src-tauri/src/commands/java2lce/region.rs
new file mode 100644
index 0000000..6ac5b2c
--- /dev/null
+++ b/src-tauri/src/commands/java2lce/region.rs
@@ -0,0 +1,803 @@
+use std::collections::HashMap;
+use std::fs;
+use std::io::{Read, Write};
+use std::path::Path;
+use flate2::read::ZlibDecoder;
+use flate2::write::ZlibEncoder;
+use flate2::Compression;
+use super::nbt::{self, NbtCompound, NbtValue};
+const SECTOR_BYTES: usize = 4096;
+pub struct JavaWorldReader {
+ world_path: String,
+ region_readers: HashMap,
+}
+
+pub struct JavaRegionReader {
+ pub data: Vec,
+ pub offsets: [u32; 1024],
+}
+
+impl JavaWorldReader {
+ pub fn new(world_path: &str) -> Self {
+ Self {
+ world_path: world_path.to_string(),
+ region_readers: HashMap::new(),
+ }
+ }
+
+ pub fn read_level_dat(&self) -> Result {
+ let path = Path::new(&self.world_path).join("level.dat");
+ let data = fs::read(&path).map_err(|e| format!("Failed to read level.dat: {}", e))?;
+ nbt::read_gzip_nbt(&data)
+ }
+
+ pub fn get_region_dir(&self, dimension: &str) -> String {
+ if dimension.is_empty() {
+ Path::new(&self.world_path)
+ .join("region")
+ .to_string_lossy()
+ .to_string()
+ } else {
+ Path::new(&self.world_path)
+ .join(dimension)
+ .join("region")
+ .to_string_lossy()
+ .to_string()
+ }
+ }
+
+ pub fn get_region_files(&self, dimension: &str) -> Vec<(i32, i32, String)> {
+ let mut result = Vec::new();
+ let dir = self.get_region_dir(dimension);
+ let dir_path = Path::new(&dir);
+ if !dir_path.is_dir() {
+ return result;
+ }
+
+ if let Ok(entries) = fs::read_dir(dir_path) {
+ for entry in entries.flatten() {
+ let name = entry.file_name().to_string_lossy().to_string();
+ let lower = name.to_lowercase();
+ if lower.ends_with(".mcr") || lower.ends_with(".mca") {
+ let parts: Vec<&str> = name.split('.').collect();
+ if parts.len() == 4 && parts[0] == "r" {
+ if let (Ok(rx), Ok(rz)) =(parts[1].parse::(), parts[2].parse::())
+ {
+ result.push((rx, rz, entry.path().to_string_lossy().to_string()));
+ }
+ }
+ }
+ }
+ }
+
+ result
+ }
+
+ pub fn is_anvil_world(&self) -> bool {
+ let region_dir = self.get_region_dir("");
+ let dir_path = Path::new(®ion_dir);
+ if !dir_path.is_dir() {
+ return false;
+ }
+ if let Ok(entries) = fs::read_dir(dir_path) {
+ for entry in entries.flatten() {
+ let name = entry.file_name().to_string_lossy().to_string();
+ if name.to_lowercase().ends_with(".mca") {
+ return true;
+ }
+ }
+ }
+ false
+ }
+
+ fn get_or_create_reader(&mut self, region_path: &str) -> Result<&JavaRegionReader, String> {
+ if !self.region_readers.contains_key(region_path) {
+ let reader = JavaRegionReader::open(region_path)?;
+ self.region_readers.insert(region_path.to_string(), reader);
+ }
+ Ok(self.region_readers.get(region_path).unwrap())
+ }
+
+ pub fn has_chunk(&mut self, region_path: &str, local_x: i32, local_z: i32) -> bool {
+ let reader = match self.get_or_create_reader(region_path) {
+ Ok(r) => r,
+ Err(_) => return false,
+ };
+ let index = (local_x & 31) + (local_z & 31) * 32;
+ reader.offsets[index as usize] != 0
+ }
+
+ pub fn read_chunk_nbt(
+ &mut self,
+ region_path: &str,
+ local_x: i32,
+ local_z: i32,
+ ) -> Result