feat: initial support for java world conversion

This commit is contained in:
neoapps-dev 2026-07-17 20:53:29 +03:00
parent 159bb5705d
commit 9318d7d50f
12 changed files with 5620 additions and 2 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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<i32>,
) -> Vec<u8> {
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<i32>,
override_spawn_y: Option<i32>,
override_spawn_z: Option<i32>,
embedded_player: Option<&NbtCompound>,
) -> Vec<u8> {
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
}

View file

@ -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<String, String> {
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<String> = 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(&region_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<String, String> {
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<u8>)> = 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<String> = Vec::new();
let mut region_writers: HashMap<String, JavaRegionFileWriter> = 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(&section_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(&region_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)
}

View file

@ -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<u8>),
String(String),
List(Vec<NbtValue>),
Compound(NbtCompound),
IntArray(Vec<i32>),
LongArray(Vec<i64>),
}
#[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<i8> {
self.get(name).and_then(|v| match v {
NbtValue::Byte(b) => Some(*b),
_ => None,
})
}
pub fn short(&self, name: &str) -> Option<i16> {
self.get(name).and_then(|v| match v {
NbtValue::Short(s) => Some(*s),
_ => None,
})
}
pub fn int(&self, name: &str) -> Option<i32> {
self.get(name).and_then(|v| match v {
NbtValue::Int(i) => Some(*i),
_ => None,
})
}
pub fn long(&self, name: &str) -> Option<i64> {
self.get(name).and_then(|v| match v {
NbtValue::Long(l) => Some(*l),
_ => None,
})
}
pub fn float(&self, name: &str) -> Option<f32> {
self.get(name).and_then(|v| match v {
NbtValue::Float(f) => Some(*f),
_ => None,
})
}
pub fn double(&self, name: &str) -> Option<f64> {
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<NbtCompound, String> {
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<u8> {
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<u8>, 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<u8>, 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<u8>, 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<NbtCompound, String> {
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<u8> {
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<NbtCompound, String> {
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<u8> {
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<u8> {
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<i32> {
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<i64> {
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<NbtValue> {
compound
.list(name)
.map(|l| l.to_vec())
.unwrap_or_default()
}

View file

@ -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<u8>, v: i16) {
out.extend_from_slice(&v.to_be_bytes());
}
fn write_be_i32(out: &mut Vec<u8>, v: i32) {
out.extend_from_slice(&v.to_be_bytes());
}
fn write_be_i64(out: &mut Vec<u8>, 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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8>, 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<u8>) {
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<u8>, 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<u8>> = 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<u8>, 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<u8> {
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<u8> {
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<Vec<u8>, 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<Vec<u8>, 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<u8> {
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<Vec<u8>> {
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<nbt::NbtCompound, String> {
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<usize> {
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)
}

View file

@ -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<String, JavaRegionReader>,
}
pub struct JavaRegionReader {
pub data: Vec<u8>,
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<NbtCompound, String> {
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::<i32>(), parts[2].parse::<i32>())
{
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(&region_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<Option<NbtCompound>, String> {
let reader = self.get_or_create_reader(region_path)?;
let index = (local_x & 31) + (local_z & 31) * 32;
let offset_entry = reader.offsets[index as usize];
if offset_entry == 0 {
return Ok(None);
}
let sector_offset = (offset_entry >> 8) as usize;
let chunk_pos = sector_offset * SECTOR_BYTES;
let data_len = reader.data.len();
if chunk_pos + 5 > data_len {
return Ok(None);
}
let length = u32::from_be_bytes([
reader.data[chunk_pos],
reader.data[chunk_pos + 1],
reader.data[chunk_pos + 2],
reader.data[chunk_pos + 3],
]) as usize;
let compression_type = reader.data[chunk_pos + 4];
if length <= 1 {
return Ok(None);
}
let compressed_length = length - 1;
if chunk_pos + 5 + compressed_length > data_len {
return Ok(None);
}
let compressed_data = &reader.data[chunk_pos + 5..chunk_pos + 5 + compressed_length];
let decompressed = match compression_type {
1 => {
let mut decoder = flate2::read::GzDecoder::new(compressed_data);
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).map_err(|e| format!("gzip decompress failed: {}", e))?;
buf
}
2 => {
let mut decoder = ZlibDecoder::new(compressed_data);
let mut buf = Vec::new();
decoder
.read_to_end(&mut buf)
.map_err(|e| format!("zlib decompress failed: {}", e))?;
buf
}
_ => return Ok(None),
};
if decompressed.is_empty() {
return Ok(None);
}
let compound = nbt::read_nbt(&decompressed)?;
Ok(Some(compound))
}
}
impl JavaRegionReader {
fn open(path: &str) -> Result<Self, String> {
let data = fs::read(path).map_err(|e| format!("Failed to read region file: {}", e))?;
let mut offsets = [0u32; 1024];
for i in 0..1024 {
let off = i * 4;
if off + 4 > data.len() {
break;
}
offsets[i] = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]);
}
Ok(Self { data, offsets })
}
pub fn open_from_bytes(data: &[u8]) -> Self {
let data = data.to_vec();
let mut offsets = [0u32; 1024];
for i in 0..1024 {
let off = i * 4;
if off + 4 > data.len() {
break;
}
offsets[i] = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]);
}
Self { data, offsets }
}
pub fn read_chunk(
&mut self,
local_x: i32,
local_z: i32,
) -> Result<Option<Vec<u8>>, String> {
let index = (local_x & 31) + (local_z & 31) * 32;
let offset_entry = self.offsets[index as usize];
if offset_entry == 0 {
return Ok(None);
}
let sector_offset = (offset_entry >> 8) as usize;
let chunk_pos = sector_offset * SECTOR_BYTES;
let data_len = self.data.len();
if chunk_pos + 5 > data_len {
return Ok(None);
}
let length = u32::from_be_bytes([
self.data[chunk_pos],
self.data[chunk_pos + 1],
self.data[chunk_pos + 2],
self.data[chunk_pos + 3],
]) as usize;
let compression_type = self.data[chunk_pos + 4];
if length <= 1 {
return Ok(None);
}
let compressed_length = length - 1;
if chunk_pos + 5 + compressed_length > data_len {
return Ok(None);
}
let compressed_data = &self.data[chunk_pos + 5..chunk_pos + 5 + compressed_length];
let decompressed = match compression_type {
1 => {
let mut decoder = flate2::read::GzDecoder::new(compressed_data);
let mut buf = Vec::new();
decoder
.read_to_end(&mut buf)
.map_err(|e| format!("gzip decompress failed: {}", e))?;
buf
}
2 => {
let mut decoder = ZlibDecoder::new(compressed_data);
let mut buf = Vec::new();
decoder
.read_to_end(&mut buf)
.map_err(|e| format!("zlib decompress failed: {}", e))?;
buf
}
_ => return Ok(None),
};
if decompressed.is_empty() {
return Ok(None);
}
Ok(Some(decompressed))
}
}
pub struct JavaRegionFileWriter {
path: String,
buffer: Vec<u8>,
offsets: [i32; 1024],
timestamps: [i32; 1024],
next_sector: usize,
}
impl JavaRegionFileWriter {
pub fn new(path: &str) -> Self {
let buffer = vec![0u8; SECTOR_BYTES * 2];
let offsets = [0i32; 1024];
let timestamps = [0i32; 1024];
Self {
path: path.to_string(),
buffer,
offsets,
timestamps,
next_sector: 2,
}
}
pub fn load_from_file(path: &str) -> Self {
if let Ok(data) = fs::read(path) {
let buffer = data.clone();
let mut offsets = [0i32; 1024];
let mut timestamps = [0i32; 1024];
for i in 0..1024 {
let pos = i * 4;
if pos + 4 <= data.len() {
offsets[i] = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
}
}
for i in 0..1024 {
let pos = SECTOR_BYTES + i * 4;
if pos + 4 <= data.len() {
timestamps[i] = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
}
}
let mut max_sector = 2;
for &off in &offsets {
if off != 0 {
let sector = (off >> 8) as usize;
let count = (off & 0xFF) as usize;
let end = sector + count;
if end > max_sector {
max_sector = end;
}
}
}
Self {
path: path.to_string(),
buffer,
offsets,
timestamps,
next_sector: max_sector,
}
} else {
Self::new(path)
}
}
pub fn write_chunk(&mut self, local_x: i32, local_z: i32, uncompressed_nbt: &[u8]) {
if local_x < 0 || local_x > 31 || local_z < 0 || local_z > 31 {
return;
}
let compressed = compress_zlib(uncompressed_nbt);
let payload_length = 1 + compressed.len();
let total_length = 4 + payload_length;
let sectors_needed = (total_length + SECTOR_BYTES - 1) / SECTOR_BYTES;
if sectors_needed >= 256 {
return;
}
let sector_start = self.next_sector;
self.next_sector += sectors_needed;
while self.buffer.len() < (sector_start + sectors_needed) * SECTOR_BYTES {
self.buffer.extend(std::iter::repeat(0u8).take(SECTOR_BYTES));
}
let write_pos = sector_start * SECTOR_BYTES;
self.buffer[write_pos..write_pos + 4]
.copy_from_slice(&(payload_length as u32).to_be_bytes());
self.buffer[write_pos + 4] = 2;
self.buffer[write_pos + 5..write_pos + 5 + compressed.len()]
.copy_from_slice(&compressed);
let padding = sectors_needed * SECTOR_BYTES - total_length;
if padding > 0 {
for i in 0..padding {
self.buffer[write_pos + total_length + i] = 0;
}
}
let offset_index = (local_x & 31) + (local_z & 31) * 32;
self.offsets[offset_index as usize] = ((sector_start as i32) << 8) | (sectors_needed as i32);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i32;
self.timestamps[offset_index as usize] = now;
}
pub fn save(&mut self) -> Result<(), String> {
while self.buffer.len() < self.next_sector * SECTOR_BYTES {
self.buffer.extend(std::iter::repeat(0u8).take(SECTOR_BYTES));
}
for i in 0..1024 {
let pos = i * 4;
self.buffer[pos..pos + 4].copy_from_slice(&(self.offsets[i] as u32).to_be_bytes());
}
for i in 0..1024 {
let pos = SECTOR_BYTES + i * 4;
self.buffer[pos..pos + 4]
.copy_from_slice(&(self.timestamps[i] as u32).to_be_bytes());
}
let total = self.next_sector * SECTOR_BYTES;
let data = &self.buffer[..total];
if let Some(parent) = Path::new(&self.path).parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create region directory: {}", e))?;
}
fs::write(&self.path, data)
.map_err(|e| format!("Failed to write region file: {}", e))?;
Ok(())
}
}
pub struct LceRegionFile {
filename: String,
data: Vec<u8>,
offsets: [i32; 1024],
timestamps: [i32; 1024],
region_x: i32,
region_z: i32,
sector_count: usize,
}
impl LceRegionFile {
pub fn new(filename: &str) -> Self {
let (region_x, region_z) = parse_region_coordinates(filename);
let data = vec![0u8; SECTOR_BYTES * 2];
Self {
filename: filename.to_string(),
data,
offsets: [0i32; 1024],
timestamps: [0i32; 1024],
region_x,
region_z,
sector_count: 2,
}
}
pub fn write_chunk(&mut self, x: i32, z: i32, mut uncompressed_data: Vec<u8>) {
if x < 0 || x >= 32 || z < 0 || z >= 32 {
return;
}
if uncompressed_data.is_empty() {
return;
}
uncompressed_data = force_chunk_coordinates(
&uncompressed_data,
self.region_x * 32 + x,
self.region_z * 32 + z,
);
if uncompressed_data.is_empty() {
return;
}
let compressed = compress_zlib_only(&uncompressed_data);
let total_size = 8 + compressed.len();
let sectors_needed = (total_size + SECTOR_BYTES - 1) / SECTOR_BYTES;
if sectors_needed >= 256 {
return;
}
let sector_number = self.sector_count;
self.sector_count += sectors_needed;
while self.data.len() < (sector_number + sectors_needed) * SECTOR_BYTES {
self.data.extend(std::iter::repeat(0u8).take(SECTOR_BYTES));
}
let write_pos = sector_number * SECTOR_BYTES;
let stored_length = compressed.len() as u32;
self.data[write_pos..write_pos + 4]
.copy_from_slice(&stored_length.to_le_bytes());
self.data[write_pos + 4..write_pos + 8]
.copy_from_slice(&(uncompressed_data.len() as u32).to_le_bytes());
self.data[write_pos + 8..write_pos + 8 + compressed.len()]
.copy_from_slice(&compressed);
let padding = sectors_needed * SECTOR_BYTES - total_size;
if padding > 0 {
for i in 0..padding {
self.data[write_pos + total_size + i] = 0;
}
}
let offset_index = (x + z * 32) as usize;
self.offsets[offset_index] = ((sector_number as i32) << 8) | (sectors_needed as i32);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i32;
self.timestamps[offset_index] = now;
}
pub fn write_to_container(&mut self, container: &mut SaveDataContainer) {
let _sorted_entries: Vec<(usize, i32, i32)> = (0..1024)
.map(|i| (i, self.offsets[i], self.timestamps[i]))
.filter(|(_, off, _)| *off != 0)
.collect();
for i in 0..1024 {
let pos = i * 4;
self.data[pos..pos + 4].copy_from_slice(&(self.offsets[i] as u32).to_le_bytes());
}
for i in 0..1024 {
let pos = SECTOR_BYTES + i * 4;
self.data[pos..pos + 4]
.copy_from_slice(&(self.timestamps[i] as u32).to_le_bytes());
}
let total = self.sector_count * SECTOR_BYTES;
let region_bytes = self.data[..total].to_vec();
let entry = container.create_file(&self.filename);
container.write_to_file(entry, &region_bytes);
}
}
fn parse_region_coordinates(filename: &str) -> (i32, i32) {
let name = Path::new(filename)
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let parts: Vec<&str> = name.split('.').collect();
if parts.len() < 3 {
return (0, 0);
}
let rx = parts[parts.len() - 2].parse().unwrap_or(0);
let rz = parts[parts.len() - 1].parse().unwrap_or(0);
(rx, rz)
}
pub fn force_chunk_coordinates(data: &[u8], expected_x: i32, expected_z: i32) -> Vec<u8> {
if data.is_empty() {
return Vec::new();
}
if let Ok(mut compound) = nbt::read_nbt(data) {
let has_level = compound.get("Level").is_some();
if has_level {
if let Some(nbt::NbtValue::Compound(ref mut c)) = compound.get_mut("Level") {
c.insert("xPos", NbtValue::Int(expected_x));
c.insert("zPos", NbtValue::Int(expected_z));
}
} else {
compound.insert("xPos", NbtValue::Int(expected_x));
compound.insert("zPos", NbtValue::Int(expected_z));
}
let mut root = NbtCompound::new("");
root.insert("Level", NbtValue::Compound(compound));
nbt::write_nbt(&root)
} else if data.len() >= 10 {
let version = i16::from_be_bytes([data[0], data[1]]);
if version == 8 || version == 9 {
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()
} else {
Vec::new()
}
}
pub struct SaveDataContainer {
original_save_version: i16,
current_save_version: i16,
entries: Vec<SaveFileEntry>,
blob: Vec<u8>,
data_end: usize,
}
pub struct SaveFileEntry {
pub name: String,
pub length: u32,
pub start_offset: u32,
pub last_mod: i64,
pub current_pointer: u32,
}
impl SaveDataContainer {
pub const FILE_ENTRY_SIZE: usize = 144;
pub fn new(original_save_version: i16, current_save_version: i16) -> Self {
Self {
original_save_version,
current_save_version,
entries: Vec::new(),
blob: vec![0u8; 2 * 1024 * 1024],
data_end: 12,
}
}
pub fn create_file(&mut self, name: &str) -> usize {
if let Some(idx) = self.entries.iter().position(|e| e.name == name) {
return idx;
}
let entry = SaveFileEntry {
name: name.to_string(),
length: 0,
start_offset: self.data_end as u32,
last_mod: 0,
current_pointer: self.data_end as u32,
};
self.entries.push(entry);
self.entries.len() - 1
}
pub fn write_to_file(&mut self, index: usize, data: &[u8]) {
if data.is_empty() {
return;
}
let needs_reinit = self.entries[index].length == 0;
if needs_reinit {
self.entries[index].start_offset = self.data_end as u32;
self.entries[index].current_pointer = self.data_end as u32;
}
let write_pos = self.entries[index].current_pointer as usize;
let end_pos = write_pos + data.len();
self.ensure_capacity(end_pos);
self.blob[write_pos..write_pos + data.len()].copy_from_slice(data);
self.entries[index].current_pointer += data.len() as u32;
let written = self.entries[index].current_pointer - self.entries[index].start_offset;
if written > self.entries[index].length {
self.entries[index].length = written;
}
let entry_end = (self.entries[index].start_offset + self.entries[index].length) as usize;
if entry_end > self.data_end {
self.data_end = entry_end;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
self.entries[index].last_mod = now;
}
pub fn save(&mut self, output_path: &str) -> Result<(), String> {
self.write_header();
let total_size = self.data_end + self.entries.len() * Self::FILE_ENTRY_SIZE;
self.ensure_capacity(total_size);
self.write_footer();
let raw_blob = self.blob[..total_size].to_vec();
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
encoder
.write_all(&raw_blob)
.map_err(|e| format!("zlib compress failed: {}", e))?;
let compressed_data = encoder
.finish()
.map_err(|e| format!("zlib finish failed: {}", e))?;
if let Some(parent) = Path::new(output_path).parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create output directory: {}", e))?;
}
let mut file = fs::File::create(output_path)
.map_err(|e| format!("Failed to create output file: {}", e))?;
file.write_all(&(0u32).to_le_bytes())
.map_err(|e| format!("write failed: {}", e))?;
file.write_all(&(total_size as u32).to_le_bytes())
.map_err(|e| format!("write failed: {}", e))?;
file.write_all(&compressed_data)
.map_err(|e| format!("write failed: {}", e))?;
Ok(())
}
fn write_header(&mut self) {
self.blob[0..4]
.copy_from_slice(&(self.data_end as u32).to_le_bytes());
self.blob[4..8]
.copy_from_slice(&(self.entries.len() as u32).to_le_bytes());
self.blob[8..10]
.copy_from_slice(&self.original_save_version.to_le_bytes());
self.blob[10..12]
.copy_from_slice(&self.current_save_version.to_le_bytes());
}
fn write_footer(&mut self) {
let mut sorted: Vec<usize> = (0..self.entries.len()).collect();
sorted.sort_by_key(|&i| self.entries[i].start_offset);
let mut pos = self.data_end;
for &i in &sorted {
let entry = &self.entries[i];
let mut name_bytes = vec![0u8; 128];
let encoded: Vec<u8> = entry
.name
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect();
let copy_len = encoded.len().min(126);
name_bytes[..copy_len].copy_from_slice(&encoded[..copy_len]);
self.blob[pos..pos + 128].copy_from_slice(&name_bytes);
pos += 128;
self.blob[pos..pos + 4].copy_from_slice(&entry.length.to_le_bytes());
pos += 4;
self.blob[pos..pos + 4].copy_from_slice(&entry.start_offset.to_le_bytes());
pos += 4;
self.blob[pos..pos + 8].copy_from_slice(&entry.last_mod.to_le_bytes());
pos += 8;
}
}
fn ensure_capacity(&mut self, required: usize) {
if required <= self.blob.len() {
return;
}
let mut new_size = self.blob.len();
while new_size < required {
new_size *= 2;
}
self.blob.resize(new_size, 0);
}
}
pub fn compress_zlib(data: &[u8]) -> Vec<u8> {
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
encoder.write_all(data).unwrap();
encoder.finish().unwrap()
}
pub fn compress_zlib_only(data: &[u8]) -> Vec<u8> {
compress_zlib(data)
}
pub fn decompress_zlib(data: &[u8]) -> Result<Vec<u8>, String> {
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))?;
Ok(buf)
}
pub fn decompress_rle_zlib(data: &[u8], decompressed_size: usize) -> Result<Vec<u8>, String> {
let rle_data = decompress_zlib(data)?;
Ok(rle_decode(&rle_data, decompressed_size))
}
pub fn rle_encode(data: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let mut i = 0;
while i < data.len() {
let current = data[i];
i += 1;
let mut count = 1usize;
while i < data.len() && data[i] == current && count < 256 {
i += 1;
count += 1;
}
if count <= 3 {
if current == 0xFF {
out.push(0xFF);
out.push((count - 1) as u8);
} else {
for _ in 0..count {
out.push(current);
}
}
} else {
out.push(0xFF);
out.push((count - 1) as u8);
out.push(current);
}
}
out
}
pub fn rle_decode(data: &[u8], expected_size: usize) -> Vec<u8> {
let mut output = vec![0u8; expected_size];
let mut in_pos = 0;
let mut out_pos = 0;
while in_pos < data.len() && out_pos < expected_size {
let current = data[in_pos];
in_pos += 1;
if current == 0xFF {
if in_pos >= data.len() {
break;
}
let count = data[in_pos] as usize;
in_pos += 1;
if count < 3 {
let run = count + 1;
for _ in 0..run {
if out_pos < expected_size {
output[out_pos] = 0xFF;
out_pos += 1;
}
}
} else {
let run = count + 1;
if in_pos >= data.len() {
break;
}
let value = data[in_pos];
in_pos += 1;
for _ in 0..run {
if out_pos < expected_size {
output[out_pos] = value;
out_pos += 1;
}
}
}
} else {
output[out_pos] = current;
out_pos += 1;
}
}
output
}

View file

@ -4,6 +4,7 @@ pub mod dlc;
pub mod download;
pub mod file_dialogs;
pub mod game;
pub mod java2lce;
pub mod macos_setup;
pub mod plugins;
pub mod proxy_cmd;

View file

@ -109,6 +109,8 @@ pub fn run() {
game::restore_instance,
commands::console2lce::import_world,
commands::console2lce::import_lce_save,
commands::java2lce::java_to_lce,
commands::java2lce::lce_to_java,
stun::stun_discover,
relay::start_relay_proxy,
relay::start_host_relay,

View file

@ -1,8 +1,6 @@
use tauri::State;
use tauri::webview::cookie::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::time::sleep;
use tokio_util::sync::CancellationToken;
use crate::state::ProxyGuard;
const PROXY_ADDR: &str = "proxy.mclegacyedition.xyz:2052"; //neo: yeah bro im hardcoding it

View file

@ -120,6 +120,70 @@ export default function ImportWorldModal({
}
};
const handleImportJava = async () => {
if (!targetInstanceId) return;
playPressSound();
setIsImporting(true);
setError("");
setStatus("Selecting source...");
try {
setStatus("Selecting Java world folder...");
const picked = await TauriService.pickFolder();
if (!picked) {
setIsImporting(false);
return;
}
const worldName = deriveWorldName(picked);
setStatus("Converting Java world to LCE...");
const instancePath = await TauriService.getInstancePath(targetInstanceId);
const saveDir = `${instancePath}/Windows64/GameHDD/${worldName}`;
await TauriService.javaToLce(picked, `${saveDir}/saveData.ms`);
setStatus(`Java world converted into "${targetInstanceName}"!`);
setTimeout(() => {
onClose();
}, 2000);
} catch (e: unknown) {
setError(e instanceof Error ? e.message : String(e));
setStatus("");
setIsImporting(false);
}
};
const handleExportJava = async () => {
if (!targetInstanceId) return;
playPressSound();
setIsImporting(true);
setError("");
setStatus("Selecting source...");
try {
setStatus("Selecting saveData.ms file...");
const picked = await TauriService.pickFile("Select saveData.ms", [
"*.ms",
"*",
]);
if (!picked) {
setIsImporting(false);
return;
}
setStatus("Selecting output folder for Java world...");
const outputFolder = await TauriService.pickFolder();
if (!outputFolder) {
setIsImporting(false);
return;
}
setStatus("Converting LCE save to Java world...");
await TauriService.lceToJava(picked, outputFolder);
setStatus(`Java world exported to "${outputFolder}"!`);
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();
@ -204,6 +268,34 @@ export default function ImportWorldModal({
</button>
</div>
<p className="text-gray-400 text-xs mc-text-shadow mb-2 text-center">
Java Edition world conversion:
</p>
<div className="flex gap-3 mb-2">
<button
onClick={handleImportJava}
className="w-40 h-9 flex items-center justify-center text-sm text-white mc-text-shadow hover:text-[#FFFF55]"
style={{
backgroundImage: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Java LCE
</button>
<button
onClick={handleExportJava}
className="w-40 h-9 flex items-center justify-center text-sm text-white mc-text-shadow hover:text-[#FFFF55]"
style={{
backgroundImage: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
LCE Java
</button>
</div>
{error && (
<div className="text-red-500 text-center mc-text-shadow uppercase text-xs tracking-widest mb-3">
{error}

View file

@ -427,4 +427,18 @@ export class TauriService {
): Promise<string> {
return invoke("import_lce_save", { inputPath, outputDir });
}
static async javaToLce(
javaWorldPath: string,
outputMsPath: string,
): Promise<string> {
return invoke("java_to_lce", { javaWorldPath, outputMsPath });
}
static async lceToJava(
inputMsPath: string,
javaWorldOutput: string,
): Promise<string> {
return invoke("lce_to_java", { inputMsPath, javaWorldOutput });
}
}