mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-07-18 08:17:23 +00:00
feat: initial support for java world conversion
This commit is contained in:
parent
159bb5705d
commit
9318d7d50f
1353
src-tauri/src/commands/java2lce/block_mapping.rs
Normal file
1353
src-tauri/src/commands/java2lce/block_mapping.rs
Normal file
File diff suppressed because it is too large
Load diff
1690
src-tauri/src/commands/java2lce/chunk.rs
Normal file
1690
src-tauri/src/commands/java2lce/chunk.rs
Normal file
File diff suppressed because it is too large
Load diff
251
src-tauri/src/commands/java2lce/level_dat.rs
Normal file
251
src-tauri/src/commands/java2lce/level_dat.rs
Normal 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
|
||||
}
|
||||
235
src-tauri/src/commands/java2lce/mod.rs
Normal file
235
src-tauri/src/commands/java2lce/mod.rs
Normal 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(®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<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(§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)
|
||||
}
|
||||
496
src-tauri/src/commands/java2lce/nbt.rs
Normal file
496
src-tauri/src/commands/java2lce/nbt.rs
Normal 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()
|
||||
}
|
||||
683
src-tauri/src/commands/java2lce/payload.rs
Normal file
683
src-tauri/src/commands/java2lce/payload.rs
Normal 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)
|
||||
}
|
||||
803
src-tauri/src/commands/java2lce/region.rs
Normal file
803
src-tauri/src/commands/java2lce/region.rs
Normal 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(®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<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, ®ion_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
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue