chore: remove console2lce because IT DOESNT WORK

This commit is contained in:
neoapps-dev 2026-05-24 11:27:21 +03:00
parent f85829c88c
commit 3a7335f43d
21 changed files with 33 additions and 3816 deletions

View file

@ -1,7 +1,6 @@
use std::path::Path;
use std::fs;
use tauri::AppHandle;
use crate::console2lce;
#[tauri::command]
#[allow(non_snake_case)]
@ -9,69 +8,17 @@ pub async fn import_world(
_app: AppHandle,
input_path: String,
output_path: String,
profile: Option<String>,
preserve_entities: Option<bool>,
) -> Result<String, String> {
eprintln!("[import_world] called");
eprintln!("[import_world] input_path: {:?}", input_path);
eprintln!("[import_world] output_path: {:?}", output_path);
eprintln!("[import_world] profile: {:?}", profile);
let options = console2lce::ConversionOptions {
profile: profile.unwrap_or_else(|| "large".to_string()),
preserve_entities: preserve_entities.unwrap_or(false),
..Default::default()
};
let input = Path::new(&input_path);
eprintln!("[import_world] input exists: {:?}", input.exists());
eprintln!("[import_world] input is_dir: {:?}", input.is_dir());
let output_parent = Path::new(&output_path).parent();
if let Some(parent) = output_parent {
eprintln!("[import_world] output parent: {:?}", parent);
if !parent.exists() {
eprintln!("[import_world] creating output parent dir...");
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create output directory {:?}: {}", parent, e))?;
eprintln!("[import_world] output parent dir created");
}
}
let input_lower = input_path.to_lowercase();
let result = if input_lower.ends_with(".ms") {
eprintln!("[import_world] detected .ms LCE save, copying directly");
fs::copy(&input_path, &output_path)
.map_err(|e| format!("Failed to copy .ms file: {}", e))?;
console2lce::ConversionResult {
success: true,
message: "LCE save copied successfully".to_string(),
chunk_count: 0,
unknown_blocks: Vec::new(),
}
} else if input.is_dir() {
eprintln!("[import_world] detected Java world directory");
console2lce::convert_java_world_to_lce(&input_path, &output_path, &options)
.map_err(|e| {
eprintln!("[import_world] Java world conversion FAILED: {}", e);
format!("Java world conversion failed: {}", e)
})?
} else {
eprintln!("[import_world] detected Xbox 360 / savegame file");
console2lce::convert_xbox360_save_to_lce(&input_path, &output_path, &options)
.map_err(|e| {
eprintln!("[import_world] conversion FAILED: {}", e);
format!("Xbox/STFS conversion failed: {}", e)
})?
};
fs::copy(&input_path, &output_path)
.map_err(|e| format!("Failed to copy .ms file: {}", e))?;
eprintln!("[import_world] conversion succeeded, {} chunks", result.chunk_count);
let mut msg = format!("World imported successfully!\nChunks converted: {}\nOutput: {}",
result.chunk_count, output_path);
if !result.unknown_blocks.is_empty() {
msg.push_str(&format!("\n\nUnknown blocks (mapped to air):\n{}", result.unknown_blocks.join("\n")));
}
Ok(msg)
Ok(format!("World imported successfully!\nOutput: {}", output_path))
}

View file

@ -1,192 +0,0 @@
use std::collections::HashMap;
use crate::console2lce::error::ConversionError;
use crate::console2lce::models::ArchiveEntry;
const ARCHIVE_MAGIC: [u8; 8] = [0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00];
pub fn parse_file_listing(data: &[u8]) -> Result<HashMap<String, Vec<u8>>, ConversionError> {
if data.len() < 12 {
return Err(ConversionError::InvalidFormat("File listing too short".to_string()));
}
let (header, le) = match try_parse_listing_header(data, true) {
Some(h) => (h, false),
None => match try_parse_listing_header(data, false) {
Some(h) => (h, true),
None => return Err(ConversionError::InvalidFormat("Invalid file listing header".to_string())),
},
};
let footer_entry_size: usize = if header.latest <= 1 { 136 } else { 144 };
let mut files = HashMap::new();
for i in 0..header.file_count {
let entry_off = header.index_offset as usize + i as usize * footer_entry_size;
if entry_off + footer_entry_size > data.len() {
break;
}
let name = decode_utf16le_padded(&data[entry_off..entry_off + 128]);
let size_off = entry_off + 128;
let ofs_off = size_off + 4;
let file_size = if le {
u32::from_le_bytes(data[size_off..size_off + 4].try_into().unwrap())
} else {
u32::from_be_bytes(data[size_off..size_off + 4].try_into().unwrap())
};
let file_ofs = if le {
u32::from_le_bytes(data[ofs_off..ofs_off + 4].try_into().unwrap())
} else {
u32::from_be_bytes(data[ofs_off..ofs_off + 4].try_into().unwrap())
};
if name.is_empty() {
continue;
}
let start = file_ofs as usize;
let end = (start + file_size as usize).min(data.len());
if start < end {
files.insert(name, data[start..end].to_vec());
}
}
Ok(files)
}
struct ListingHeader {
index_offset: u32,
file_count: u32,
oldest: u16,
latest: u16,
}
fn try_parse_listing_header(data: &[u8], be: bool) -> Option<ListingHeader> {
let (index_offset, file_count) = if be {
(u32::from_be_bytes(data[0..4].try_into().ok()?),
u32::from_be_bytes(data[4..8].try_into().ok()?))
} else {
(u32::from_le_bytes(data[0..4].try_into().ok()?),
u32::from_le_bytes(data[4..8].try_into().ok()?))
};
let oldest = if be {
u16::from_be_bytes(data[8..10].try_into().ok()?)
} else {
u16::from_le_bytes(data[8..10].try_into().ok()?)
};
let latest = if be {
u16::from_be_bytes(data[10..12].try_into().ok()?)
} else {
u16::from_le_bytes(data[10..12].try_into().ok()?)
};
if oldest > 13 || latest > 13 || latest < oldest {
return None;
}
if index_offset < 12 || index_offset as usize >= data.len() {
return None;
}
let (file_count, _footer_size) = if latest <= 1 {
if file_count == 0 || file_count % 136 != 0 {
return None;
}
(file_count / 136, 136usize)
} else {
(file_count, 144usize)
};
if file_count > 32768 {
return None;
}
Some(ListingHeader { index_offset, file_count, oldest, latest })
}
fn decode_utf16le_padded(bytes: &[u8]) -> String {
let mut chars = Vec::new();
for chunk in bytes.chunks(2) {
if chunk.len() < 2 { break; }
let code_unit = u16::from_le_bytes([chunk[0], chunk[1]]);
if code_unit == 0 { break; }
if let Some(c) = char::from_u32(code_unit as u32) {
chars.push(c);
}
}
chars.into_iter().collect()
}
pub fn is_minecraft_360_archive(data: &[u8]) -> bool {
data.len() >= 8 && data[0..8] == ARCHIVE_MAGIC
}
pub fn parse_archive(data: &[u8]) -> Result<HashMap<String, ArchiveEntry>, ConversionError> {
if data.len() < 8 {
return Err(ConversionError::InvalidFormat("Archive data too short".to_string()));
}
if !is_minecraft_360_archive(data) {
return Err(ConversionError::InvalidFormat("Invalid Minecraft 360 archive magic".to_string()));
}
let num_entries = read_be_u32(&data[8..12]) as usize;
let _header_size = read_be_u32(&data[12..16]) as usize;
let mut offset = 16usize;
let mut entries = HashMap::new();
for _ in 0..num_entries {
if offset + 144 > data.len() {
break;
}
let name_bytes = &data[offset..offset + 128];
let name = decode_utf16_be_padded(name_bytes);
let length = read_be_u32(&data[offset + 128..offset + 132]);
let file_offset = read_be_u32(&data[offset + 132..offset + 136]) as u64;
let timestamp = read_be_u64(&data[offset + 136..offset + 144]);
offset += 144;
entries.insert(name.clone(), ArchiveEntry {
name,
offset: file_offset,
length,
timestamp,
});
}
Ok(entries)
}
fn decode_utf16_be_padded(bytes: &[u8]) -> String {
let mut chars = Vec::new();
for chunk in bytes.chunks(2) {
if chunk.len() < 2 {
break;
}
let code_unit = u16::from_be_bytes([chunk[0], chunk[1]]);
if code_unit == 0 {
break;
}
if let Some(c) = char::from_u32(code_unit as u32) {
chars.push(c);
}
}
chars.into_iter().collect()
}
fn read_be_u32(bytes: &[u8]) -> u32 {
if bytes.len() < 4 { return 0; }
((bytes[0] as u32) << 24) | ((bytes[1] as u32) << 16) | ((bytes[2] as u32) << 8) | (bytes[3] as u32)
}
fn read_be_u64(bytes: &[u8]) -> u64 {
if bytes.len() < 8 { return 0; }
((bytes[0] as u64) << 56) | ((bytes[1] as u64) << 48) | ((bytes[2] as u64) << 40) | ((bytes[3] as u64) << 32)
| ((bytes[4] as u64) << 24) | ((bytes[5] as u64) << 16) | ((bytes[6] as u64) << 8) | (bytes[7] as u64)
}
pub fn extract_file_from_archive(data: &[u8], entry: &ArchiveEntry) -> Result<Vec<u8>, ConversionError> {
let start = entry.offset as usize;
let end = start + entry.length as usize;
if end > data.len() {
return Err(ConversionError::InvalidFormat(format!(
"Archive entry '{}' at offset {} with length {} exceeds total size {}",
entry.name, entry.offset, entry.length, data.len()
)));
}
Ok(data[start..end].to_vec())
}

View file

@ -1,176 +0,0 @@
use std::collections::HashMap;
use once_cell::sync::Lazy;
use crate::console2lce::models::LegacyBlockState;
pub static MODERN_DIRECT_MAP: Lazy<HashMap<&'static str, LegacyBlockState>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert("stone", LegacyBlockState::new(1, 0));
m.insert("granite", LegacyBlockState::new(1, 1));
m.insert("polished_granite", LegacyBlockState::new(1, 2));
m.insert("diorite", LegacyBlockState::new(1, 3));
m.insert("polished_diorite", LegacyBlockState::new(1, 4));
m.insert("andesite", LegacyBlockState::new(1, 5));
m.insert("polished_andesite", LegacyBlockState::new(1, 6));
m.insert("grass_block", LegacyBlockState::new(2, 0));
m.insert("dirt", LegacyBlockState::new(3, 0));
m.insert("coarse_dirt", LegacyBlockState::new(3, 1));
m.insert("podzol", LegacyBlockState::new(3, 2));
m.insert("cobblestone", LegacyBlockState::new(4, 0));
m.insert("bedrock", LegacyBlockState::new(7, 0));
m.insert("sand", LegacyBlockState::new(12, 0));
m.insert("red_sand", LegacyBlockState::new(12, 1));
m.insert("gravel", LegacyBlockState::new(13, 0));
m.insert("gold_ore", LegacyBlockState::new(14, 0));
m.insert("iron_ore", LegacyBlockState::new(15, 0));
m.insert("coal_ore", LegacyBlockState::new(16, 0));
m.insert("sponge", LegacyBlockState::new(19, 0));
m.insert("wet_sponge", LegacyBlockState::new(19, 1));
m.insert("glass", LegacyBlockState::new(20, 0));
m.insert("lapis_ore", LegacyBlockState::new(21, 0));
m.insert("lapis_block", LegacyBlockState::new(22, 0));
m.insert("sandstone", LegacyBlockState::new(24, 0));
m.insert("chiseled_sandstone", LegacyBlockState::new(24, 1));
m.insert("smooth_sandstone", LegacyBlockState::new(24, 2));
m.insert("note_block", LegacyBlockState::new(25, 0));
m.insert("cobweb", LegacyBlockState::new(30, 0));
m.insert("short_grass", LegacyBlockState::new(31, 0));
m.insert("fern", LegacyBlockState::new(31, 1));
m.insert("dead_bush", LegacyBlockState::new(32, 0));
m.insert("dandelion", LegacyBlockState::new(37, 0));
m.insert("brown_mushroom", LegacyBlockState::new(39, 0));
m.insert("red_mushroom", LegacyBlockState::new(40, 0));
m.insert("gold_block", LegacyBlockState::new(41, 0));
m.insert("iron_block", LegacyBlockState::new(42, 0));
m.insert("bricks", LegacyBlockState::new(45, 0));
m.insert("tnt", LegacyBlockState::new(46, 0));
m.insert("bookshelf", LegacyBlockState::new(47, 0));
m.insert("mossy_cobblestone", LegacyBlockState::new(48, 0));
m.insert("obsidian", LegacyBlockState::new(49, 0));
m.insert("torch", LegacyBlockState::new(50, 0));
m.insert("fire", LegacyBlockState::new(51, 0));
m.insert("mob_spawner", LegacyBlockState::new(52, 0));
m.insert("chest", LegacyBlockState::new(54, 0));
m.insert("trapped_chest", LegacyBlockState::new(146, 0));
m.insert("diamond_ore", LegacyBlockState::new(56, 0));
m.insert("diamond_block", LegacyBlockState::new(57, 0));
m.insert("crafting_table", LegacyBlockState::new(58, 0));
m.insert("wheat", LegacyBlockState::new(59, 0));
m.insert("farmland", LegacyBlockState::new(60, 0));
m.insert("furnace", LegacyBlockState::new(61, 0));
m.insert("ladder", LegacyBlockState::new(65, 0));
m.insert("rail", LegacyBlockState::new(66, 0));
m.insert("snow", LegacyBlockState::new(78, 0));
m.insert("ice", LegacyBlockState::new(79, 0));
m.insert("snow_block", LegacyBlockState::new(80, 0));
m.insert("cactus", LegacyBlockState::new(81, 0));
m.insert("clay", LegacyBlockState::new(82, 0));
m.insert("sugar_cane", LegacyBlockState::new(83, 0));
m.insert("jukebox", LegacyBlockState::new(84, 0));
m.insert("pumpkin", LegacyBlockState::new(86, 0));
m.insert("netherrack", LegacyBlockState::new(87, 0));
m.insert("soul_sand", LegacyBlockState::new(88, 0));
m.insert("glowstone", LegacyBlockState::new(89, 0));
m.insert("jack_o_lantern", LegacyBlockState::new(91, 0));
m.insert("cake", LegacyBlockState::new(92, 0));
m.insert("melon", LegacyBlockState::new(103, 0));
m.insert("vine", LegacyBlockState::new(106, 0));
m.insert("mycelium", LegacyBlockState::new(110, 0));
m.insert("lily_pad", LegacyBlockState::new(111, 0));
m.insert("nether_bricks", LegacyBlockState::new(112, 0));
m.insert("nether_brick_fence", LegacyBlockState::new(113, 0));
m.insert("enchanting_table", LegacyBlockState::new(116, 0));
m.insert("brewing_stand", LegacyBlockState::new(117, 0));
m.insert("cauldron", LegacyBlockState::new(118, 0));
m.insert("end_portal", LegacyBlockState::new(119, 0));
m.insert("end_portal_frame", LegacyBlockState::new(120, 0));
m.insert("end_stone", LegacyBlockState::new(121, 0));
m.insert("dragon_egg", LegacyBlockState::new(122, 0));
m.insert("cocoa", LegacyBlockState::new(127, 0));
m.insert("emerald_ore", LegacyBlockState::new(129, 0));
m.insert("ender_chest", LegacyBlockState::new(130, 0));
m.insert("tripwire_hook", LegacyBlockState::new(131, 0));
m.insert("tripwire", LegacyBlockState::new(132, 0));
m.insert("emerald_block", LegacyBlockState::new(133, 0));
m.insert("command_block", LegacyBlockState::new(137, 0));
m.insert("beacon", LegacyBlockState::new(138, 0));
m.insert("flower_pot", LegacyBlockState::new(140, 0));
m.insert("carrots", LegacyBlockState::new(141, 0));
m.insert("potatoes", LegacyBlockState::new(142, 0));
m.insert("anvil", LegacyBlockState::new(145, 0));
m.insert("daylight_detector", LegacyBlockState::new(151, 0));
m.insert("redstone_block", LegacyBlockState::new(152, 0));
m.insert("nether_quartz_ore", LegacyBlockState::new(153, 0));
m.insert("hopper", LegacyBlockState::new(154, 0));
m.insert("slime_block", LegacyBlockState::new(165, 0));
m.insert("iron_trapdoor", LegacyBlockState::new(167, 0));
m.insert("sea_lantern", LegacyBlockState::new(169, 0));
m.insert("hay_block", LegacyBlockState::new(170, 0));
m.insert("dispenser", LegacyBlockState::new(23, 0));
m.insert("dropper", LegacyBlockState::new(158, 0));
m.insert("sticky_piston", LegacyBlockState::new(29, 0));
m.insert("piston", LegacyBlockState::new(33, 0));
m.insert("piston_head", LegacyBlockState::new(34, 0));
m.insert("redstone_lamp", LegacyBlockState::new(123, 0));
m.insert("red_nether_bricks", LegacyBlockState::new(112, 0));
m.insert("bone_block", LegacyBlockState::new(1, 0));
m.insert("observer", LegacyBlockState::new(1, 0));
m.insert("shulker_box", LegacyBlockState::new(35, 0));
m.insert("white_shulker_box", LegacyBlockState::new(35, 0));
m.insert("orange_shulker_box", LegacyBlockState::new(35, 1));
m.insert("magenta_shulker_box", LegacyBlockState::new(35, 2));
m.insert("light_blue_shulker_box", LegacyBlockState::new(35, 3));
m.insert("yellow_shulker_box", LegacyBlockState::new(35, 4));
m.insert("lime_shulker_box", LegacyBlockState::new(35, 5));
m.insert("pink_shulker_box", LegacyBlockState::new(35, 6));
m.insert("gray_shulker_box", LegacyBlockState::new(35, 7));
m.insert("light_gray_shulker_box", LegacyBlockState::new(35, 8));
m.insert("cyan_shulker_box", LegacyBlockState::new(35, 9));
m.insert("purple_shulker_box", LegacyBlockState::new(35, 10));
m.insert("blue_shulker_box", LegacyBlockState::new(35, 11));
m.insert("brown_shulker_box", LegacyBlockState::new(35, 12));
m.insert("green_shulker_box", LegacyBlockState::new(35, 13));
m.insert("red_shulker_box", LegacyBlockState::new(35, 14));
m.insert("black_shulker_box", LegacyBlockState::new(35, 15));
m
});
pub static COLOR_NAMES: &[&str] = &[
"white", "orange", "magenta", "light_blue", "yellow", "lime", "pink", "gray",
"light_gray", "cyan", "purple", "blue", "brown", "green", "red", "black",
];
pub fn get_color_name(color: &str) -> u8 {
match color {
"white" => 0,
"orange" => 1,
"magenta" => 2,
"light_blue" => 3,
"yellow" => 4,
"lime" => 5,
"pink" => 6,
"gray" => 7,
"light_gray" | "silver" => 8,
"cyan" => 9,
"purple" => 10,
"blue" => 11,
"brown" => 12,
"green" => 13,
"red" => 14,
"black" => 15,
_ => 0,
}
}
pub fn get_color_data(color: &str) -> u8 {
get_color_name(color)
}
pub fn get_wood_data(wood_type: &str) -> u8 {
match wood_type {
"spruce" => 1,
"birch" => 2,
"jungle" => 3,
"acacia" => 4,
"dark_oak" => 5,
_ => 0,
}
}

View file

@ -1,155 +0,0 @@
use std::io::Read;
use flate2::read::ZlibDecoder;
use crate::console2lce::error::ConversionError;
use crate::console2lce::models::LegacyBlockState;
pub fn decode_xbox360_chunk(chunk_data: &[u8]) -> Result<Vec<u8>, ConversionError> {
if chunk_data.len() < 5 {
return Err(ConversionError::InvalidFormat("Chunk data too short".to_string()));
}
let _compressed_size = read_be_u32(&chunk_data[0..4]);
let compression_scheme = chunk_data[4];
let payload = &chunk_data[5..];
match compression_scheme {
0 => Ok(payload.to_vec()),
1 => {
let mut decoder = ZlibDecoder::new(payload);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?;
Ok(decompressed)
}
2 => {
let is_rle = payload.len() >= 4 && (payload[0..4].iter().any(|&b| b == 0xFF));
if is_rle {
decode_rle_chunk(payload)
} else {
Ok(payload.to_vec())
}
}
_ => Err(ConversionError::DecompressionFailed(
format!("Unknown chunk compression scheme: {}", compression_scheme)
)),
}
}
fn decode_rle_chunk(data: &[u8]) -> Result<Vec<u8>, ConversionError> {
let mut output = Vec::new();
let mut pos = 0;
while pos < data.len() {
let b = data[pos];
pos += 1;
if b != 0xFF {
output.push(b);
} else {
if pos + 1 >= data.len() {
break;
}
let run_len = data[pos] as usize;
let value = data[pos + 1];
pos += 2;
for _ in 0..run_len {
output.push(value);
}
}
}
Ok(output)
}
pub fn try_decode_compressed_tile_storage(data: &[u8]) -> Result<Vec<LegacyBlockState>, ConversionError> {
if data.len() < 2048 {
return Err(ConversionError::InvalidFormat("Compressed tile storage data too short".to_string()));
}
let mut blocks: Vec<LegacyBlockState> = Vec::with_capacity(16384);
let index_table = &data[0..2048];
let mut data_offset = 2048;
for entry in index_table.chunks(2) {
let index_value = u16::from_be_bytes([entry[0], entry[1]]);
let data_type = (index_value >> 13) & 0x07;
let data_offset_val = (index_value & 0x1FFF) as usize;
match data_type {
0 => {
for _ in 0..16 {
blocks.push(LegacyBlockState::new(0, 0));
}
}
1 => {
if data_offset + 4 <= data.len() {
for i in 0..16 {
let nibble = (data[data_offset + i / 2] >> (if i % 2 == 0 { 0 } else { 4 })) & 0x0F;
blocks.push(LegacyBlockState::new(0, nibble as u8));
}
data_offset += 4;
}
}
2 => {
if data_offset + 8 <= data.len() {
for i in 0..16 {
let nibble = if data_offset + i < data.len() {
data[data_offset + i]
} else {
0
};
blocks.push(LegacyBlockState::new(nibble, 0));
}
data_offset += 8;
}
}
3 => {
if data_offset + 16 <= data.len() {
for i in 0..16 {
if data_offset + i * 2 + 1 < data.len() {
let id = data[data_offset + i * 2];
let meta = data[data_offset + i * 2 + 1];
blocks.push(LegacyBlockState::new(id, meta));
} else {
blocks.push(LegacyBlockState::new(0, 0));
}
}
data_offset += 16;
}
}
4 => {
if data_offset_val > 0 && data_offset_val < data.len() {
let sparse_offset = data_offset_val;
let mut sparse_pos = sparse_offset;
while sparse_pos < data.len() && blocks.len() < 16384 {
if sparse_pos + 3 > data.len() {
break;
}
let location = data[sparse_pos] as usize;
let block_id = data[sparse_pos + 1];
let block_data = data[sparse_pos + 2];
sparse_pos += 3;
for _ in blocks.len()..location {
blocks.push(LegacyBlockState::new(0, 0));
}
if blocks.len() < 16384 {
blocks.push(LegacyBlockState::new(block_id, block_data));
}
}
while blocks.len() < 16384 {
blocks.push(LegacyBlockState::new(0, 0));
}
} else {
for _ in 0..16 {
blocks.push(LegacyBlockState::new(0, 0));
}
}
}
_ => {
for _ in 0..16 {
blocks.push(LegacyBlockState::new(0, 0));
}
}
}
}
Ok(blocks)
}
fn read_be_u32(bytes: &[u8]) -> u32 {
if bytes.len() < 4 { return 0; }
((bytes[0] as u32) << 24) | ((bytes[1] as u32) << 16) | ((bytes[2] as u32) << 8) | (bytes[3] as u32)
}

View file

@ -1,395 +0,0 @@
use std::collections::HashMap;
use std::path::Path;
use std::fs;
use crate::console2lce::error::ConversionError;
use crate::console2lce::models::*;
use crate::console2lce::nbt_util::{self, NbtHelper};
use crate::console2lce::modern_chunk_writer;
use crate::console2lce::lce_chunk_payload;
use crate::console2lce::lce_region::LceRegionFile;
use crate::console2lce::level_dat;
use crate::console2lce::save_data_container;
use crate::console2lce::world_profile;
pub fn convert_xbox360_save_to_lce(
input_path: &str,
output_path: &str,
_options: &ConversionOptions,
) -> Result<ConversionResult, ConversionError> {
eprintln!("[convert] convert_xbox360_save_to_lce start");
eprintln!("[convert] input_path = {:?}", input_path);
eprintln!("[convert] output_path = {:?}", output_path);
let data = std::fs::read(input_path)
.map_err(|e| {
eprintln!("[convert] ERROR reading input: {}", e);
ConversionError::Io(e)
})?;
eprintln!("[convert] read {} bytes from input", data.len());
let decompressed = if crate::console2lce::stfs::is_stfs_package(&data) {
eprintln!("[convert] detected STFS package, extracting savegame.dat...");
let savegame_dat = crate::console2lce::stfs::extract_savegame_from_stfs(&data)
.map_err(|e| {
eprintln!("[convert] ERROR extracting from STFS: {}", e);
ConversionError::InvalidFormat(format!("STFS extraction failed: {}", e))
})?;
eprintln!("[convert] savegame.dat extracted, {} bytes", savegame_dat.len());
eprintln!("[convert] decompressing STFS savegame data...");
crate::console2lce::stfs::try_decompress_stfs_savegame(&savegame_dat)
.map_err(|e| {
eprintln!("[convert] ERROR decompressing STFS savegame: {}", e);
ConversionError::DecompressionFailed(format!("STFS savegame decompression failed: {}", e))
})?
} else if (data.len() >= 8 && data[0..8] == [0x0B, 0xFC, 0x4A, 0x46, 0xAE, 0x7A, 0x43, 0x2B]) || input_path.ends_with(".dat") {
eprintln!("[convert] detected raw savegame.dat, decompressing directly...");
crate::console2lce::stfs::try_decompress_savegame(&data)
.map_err(|e| {
eprintln!("[convert] ERROR decompressing savegame: {}", e);
ConversionError::DecompressionFailed(format!("Savegame decompression failed: {}", e))
})?
} else {
return Err(ConversionError::InvalidFormat(
"Not an STFS package (.bin) or savegame.dat (.dat)".to_string()
));
};
eprintln!("[convert] decompressed {} bytes", decompressed.len());
eprintln!("[convert] parsing file listing...");
let files = crate::console2lce::archive::parse_file_listing(&decompressed)
.map_err(|e| {
eprintln!("[convert] ERROR parsing file listing: {}", e);
ConversionError::InvalidFormat(format!("File listing parse failed: {}", e))
})?;
eprintln!("[convert] file listing has {} entries", files.len());
let mut level_dat_data: Vec<u8> = Vec::new();
let mut regions: HashMap<String, Vec<u8>> = HashMap::new();
let mut total_chunks = 0;
for (name, file_data) in &files {
eprintln!("[convert] file: {:?} ({} bytes)", name, file_data.len());
if name == "level.dat" || name.ends_with("/level.dat") {
level_dat_data = file_data.clone();
} else if name.ends_with(".mcr") || name.ends_with(".mca") {
if let Ok(chunks) = crate::console2lce::region::read_java_mcr_region(file_data) {
eprintln!("[convert] region {} has {} chunks", name, chunks.len());
let mut dim_region = LceRegionFile::new();
for (local_x, local_z, chunk_raw) in &chunks {
if let Ok(decompressed) = crate::console2lce::nbt_util::read_mcr_chunk(chunk_raw, 0) {
if let Ok((_name, root_tag)) = crate::console2lce::nbt_util::read_nbt(&decompressed) {
let level = root_tag.as_compound()
.and_then(|m| m.get("Level").and_then(|t| t.as_compound()))
.or_else(|| root_tag.as_compound())
.cloned()
.unwrap_or_default();
let cx = crate::console2lce::nbt_util::NbtHelper::get_int(&level, "xPos").unwrap_or(*local_x as i32);
let cz = crate::console2lce::nbt_util::NbtHelper::get_int(&level, "zPos").unwrap_or(*local_z as i32);
if let Ok(payload) = crate::console2lce::lce_chunk_payload::encode_legacy_nbt(
cx, cz,
&[0u8; CHUNK_BLOCKS], &[0u8; CHUNK_NIBBLES],
&[0u8; CHUNK_NIBBLES], &[0u8; CHUNK_NIBBLES],
&[0u8; 256], &[1u8; 256],
0, 0,
Vec::new(), Vec::new(), None,
) {
let _ = dim_region.add_chunk(cx, cz, &payload);
total_chunks += 1;
}
}
}
}
if let Ok(data) = dim_region.build() {
let region_key = format!("region/{}", name);
regions.insert(region_key, data);
}
}
}
}
eprintln!("[convert] writing output to {:?}", output_path);
if total_chunks == 0 {
eprintln!("[convert] no chunks found. copying input directly");
if let Some(parent) = std::path::Path::new(output_path).parent() {
let _ = std::fs::create_dir_all(parent);
}
std::fs::write(output_path, &data)
.map_err(ConversionError::Io)?;
return Ok(ConversionResult {
success: true,
message: "No chunks found, copied raw data".to_string(),
chunk_count: 0,
unknown_blocks: Vec::new(),
});
}
let ms_data = save_data_container::write_save_data_ms(&level_dat_data, &regions)
.map_err(|e| {
eprintln!("[convert] ERROR building saveData.ms: {}", e);
e
})?;
if let Some(parent) = std::path::Path::new(output_path).parent() {
let _ = std::fs::create_dir_all(parent);
}
std::fs::write(output_path, &ms_data)
.map_err(ConversionError::Io)?;
eprintln!("[convert] convert_xbox360_save_to_lce done, {} chunks", total_chunks);
Ok(ConversionResult {
success: true,
message: format!("Converted {} chunks from Xbox 360 save", total_chunks),
chunk_count: total_chunks,
unknown_blocks: Vec::new(),
})
}
pub fn convert_java_world_to_lce(
world_path: &str,
output_path: &str,
options: &ConversionOptions,
) -> Result<ConversionResult, ConversionError> {
eprintln!("[convert] convert_java_world_to_lce start");
eprintln!("[convert] world_path = {:?}", world_path);
eprintln!("[convert] output_path = {:?}", output_path);
eprintln!("[convert] profile = {:?}", options.profile);
let _profile = world_profile::get_profile(&options.profile)
.ok_or_else(|| {
eprintln!("[convert] ERROR: unknown profile {:?}", options.profile);
ConversionError::InvalidFormat(format!("Unknown profile: {}", options.profile))
})?;
let world_dir = Path::new(world_path);
eprintln!("[convert] world_dir resolved to {:?}", world_dir);
eprintln!("[convert] world_dir.is_dir() = {:?}", world_dir.is_dir());
if !world_dir.is_dir() {
eprintln!("[convert] ERROR: world directory not found at {:?}", world_dir);
eprintln!("[convert] Checking existence: {:?}", world_dir.exists());
return Err(ConversionError::InvalidFormat(format!("World directory not found: {}", world_path)));
}
let level_dat_path = world_dir.join("level.dat");
eprintln!("[convert] reading level.dat: {:?}", level_dat_path);
eprintln!("[convert] level.dat exists: {:?}", level_dat_path.exists());
let level_dat_data = fs::read(&level_dat_path)
.map_err(|e| {
eprintln!("[convert] ERROR reading level.dat: {}", e);
e
})?;
eprintln!("[convert] level.dat size: {} bytes", level_dat_data.len());
let mut context = ChunkConversionContext::new();
context.preserve_dynamic_chunk_data = options.preserve_entities;
eprintln!("[convert] converting level.dat...");
let lce_level_dat = level_dat::convert_level_dat(
&level_dat_data, &options.profile, &options.game_version, &mut context,
)?;
eprintln!("[convert] level.dat converted, size: {} bytes", lce_level_dat.len());
let dimensions = vec!["region".to_string(), "DIM-1/region".to_string(), "DIM1/region".to_string()];
let mut regions: HashMap<String, Vec<u8>> = HashMap::new();
let mut total_chunks = 0;
for dim_dir in &dimensions {
let dim_path = world_dir.join(dim_dir);
eprintln!("[convert] checking dimension dir: {:?} (exists: {:?})", dim_path, dim_path.is_dir());
if !dim_path.is_dir() {
eprintln!("[convert] skipping (not a directory)");
continue;
}
let mut dim_region = LceRegionFile::new();
let entries = match fs::read_dir(&dim_path) {
Ok(e) => e,
Err(e) => {
eprintln!("[convert] ERROR reading dim dir {:?}: {}", dim_path, e);
continue;
}
};
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(e) => {
eprintln!("[convert] ERROR reading dir entry: {}", e);
continue;
}
};
let path = entry.path();
let file_name = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
if !file_name.ends_with(".mca") && !file_name.ends_with(".mcr") {
eprintln!("[convert] skipping non-region file: {:?}", path);
continue;
}
eprintln!("[convert] processing region file: {:?}", path);
let region_data = match fs::read(&path) {
Ok(d) => {
eprintln!("[convert] read {} bytes", d.len());
d
}
Err(e) => {
eprintln!("[convert] ERROR reading region file: {}", e);
continue;
}
};
let chunks = match crate::console2lce::region::read_java_mcr_region(&region_data) {
Ok(c) => {
eprintln!("[convert] found {} chunks", c.len());
c
}
Err(e) => {
eprintln!("[convert] ERROR parsing region: {}", e);
continue;
}
};
for (local_x, local_z, chunk_raw) in &chunks {
eprintln!("[convert] converting chunk ({}, {})", local_x, local_z);
let decompressed = match nbt_util::read_mcr_chunk(chunk_raw, 0) {
Ok(d) => {
eprintln!("[convert] decompressed {} bytes", d.len());
d
}
Err(e) => {
eprintln!("[convert] ERROR decompressing chunk: {}", e);
continue;
}
};
let (name, root_tag) = match nbt_util::read_nbt(&decompressed) {
Ok(v) => v,
Err(e) => {
eprintln!("[convert] ERROR parsing chunk NBT: {}", e);
continue;
}
};
eprintln!("[convert] chunk root name: {:?}", name);
let level = root_tag.as_compound()
.and_then(|m| m.get("Level").and_then(|t| t.as_compound()))
.or_else(|| root_tag.as_compound())
.cloned()
.unwrap_or_default();
let source_chunk_x = NbtHelper::get_int(&level, "xPos").unwrap_or(0);
let source_chunk_z = NbtHelper::get_int(&level, "zPos").unwrap_or(0);
eprintln!("[convert] source chunk pos: ({}, {})", source_chunk_x, source_chunk_z);
let new_chunk_x = source_chunk_x;
let new_chunk_z = source_chunk_z;
let has_sections = level.contains_key("Sections");
let uses_modern_content = level.contains_key("Status") || level.contains_key("block_states");
eprintln!("[convert] has_sections: {}, uses_modern_content: {}", has_sections, uses_modern_content);
let (blocks, data, _sky_light, _block_light) = if has_sections {
let shift = context.global_modern_section_shift;
modern_chunk_writer::flatten_anvil_sections(&level, shift, &mut context)
} else {
let blocks_arr = NbtHelper::get_byte_array_or_default(&level, "Blocks", CHUNK_BLOCKS);
let data_arr = NbtHelper::get_byte_array_or_default(&level, "Data", CHUNK_NIBBLES);
let sky_arr = NbtHelper::get_byte_array_or_default(&level, "SkyLight", CHUNK_NIBBLES);
let block_arr = NbtHelper::get_byte_array_or_default(&level, "BlockLight", CHUNK_NIBBLES);
let mut bl = [0u8; CHUNK_BLOCKS];
let mut da = [0u8; CHUNK_NIBBLES];
let mut sk = [0u8; CHUNK_NIBBLES];
let mut blk = [0u8; CHUNK_NIBBLES];
bl[..blocks_arr.len().min(CHUNK_BLOCKS)].copy_from_slice(&blocks_arr[..blocks_arr.len().min(CHUNK_BLOCKS)]);
da[..data_arr.len().min(CHUNK_NIBBLES)].copy_from_slice(&data_arr[..data_arr.len().min(CHUNK_NIBBLES)]);
sk[..sky_arr.len().min(CHUNK_NIBBLES)].copy_from_slice(&sky_arr[..sky_arr.len().min(CHUNK_NIBBLES)]);
blk[..block_arr.len().min(CHUNK_NIBBLES)].copy_from_slice(&block_arr[..block_arr.len().min(CHUNK_NIBBLES)]);
(bl, da, sk, blk)
};
let height_map = NbtHelper::get_byte_array_or_default(&level, "HeightMap", HEIGHTMAP_SIZE);
let biomes_data = NbtHelper::get_byte_array_or_default(&level, "Biomes", BIOMES_SIZE);
let mut biomes_arr = [0u8; BIOMES_SIZE];
let len = biomes_data.len().min(BIOMES_SIZE);
biomes_arr[..len].copy_from_slice(&biomes_data[..len]);
if uses_modern_content {
biomes_arr.fill(1);
}
let mut hm = [0u8; HEIGHTMAP_SIZE];
let len = height_map.len().min(HEIGHTMAP_SIZE);
hm[..len].copy_from_slice(&height_map[..len]);
let last_update = NbtHelper::get_long(&level, "LastUpdate").unwrap_or(0);
let inhabited_time = NbtHelper::get_long(&level, "InhabitedTime").unwrap_or(0);
let entities = if context.preserve_dynamic_chunk_data && !uses_modern_content {
NbtHelper::get_list(&level, "Entities").unwrap_or_default()
} else {
Vec::new()
};
let tile_entities = if context.preserve_dynamic_chunk_data {
NbtHelper::get_list(&level, "TileEntities").unwrap_or_default()
} else {
Vec::new()
};
eprintln!("[convert] encoding chunk NBT...");
let chunk_payload = match lce_chunk_payload::encode_legacy_nbt(
new_chunk_x, new_chunk_z,
&blocks, &data, &[0u8; 32768], &[0u8; 32768],
&hm, &biomes_arr,
last_update, inhabited_time,
entities, tile_entities, None,
) {
Ok(p) => {
eprintln!("[convert] encoded {} bytes", p.len());
p
}
Err(e) => {
eprintln!("[convert] ERROR encoding chunk: {}", e);
continue;
}
};
if let Err(e) = dim_region.add_chunk(new_chunk_x, new_chunk_z, &chunk_payload) {
eprintln!("[convert] ERROR adding chunk to region: {}", e);
continue;
}
total_chunks += 1;
}
}
eprintln!("[convert] building region file for {:?}...", dim_dir);
let region_bytes = match dim_region.build() {
Ok(b) => {
eprintln!("[convert] region built, {} bytes", b.len());
b
}
Err(e) => {
eprintln!("[convert] ERROR building region: {}", e);
continue;
}
};
let region_key = if dim_dir == "region" {
"region/r.0.0.mcr".to_string()
} else {
format!("{}/r.0.0.mcr", dim_dir)
};
regions.insert(region_key, region_bytes);
}
eprintln!("[convert] writing saveData.ms with {} chunks", total_chunks);
let ms_data = match save_data_container::write_save_data_ms(&lce_level_dat, &regions) {
Ok(d) => {
eprintln!("[convert] saveData.ms content built, {} bytes", d.len());
d
}
Err(e) => {
eprintln!("[convert] ERROR building saveData.ms: {}", e);
return Err(e);
}
};
eprintln!("[convert] writing to {:?}", output_path);
if let Err(e) = fs::write(output_path, &ms_data) {
eprintln!("[convert] ERROR writing output file: {}", e);
return Err(ConversionError::Io(e));
}
eprintln!("[convert] done!");
Ok(ConversionResult {
success: true,
message: format!("Successfully converted {} chunks", total_chunks),
chunk_count: total_chunks,
unknown_blocks: context.unknown_blocks,
})
}

View file

@ -1,33 +0,0 @@
use std::fmt;
use std::io;
#[derive(Debug)]
pub enum ConversionError {
Io(io::Error),
InvalidFormat(String),
UnsupportedVersion(String),
NbtError(String),
MissingData(String),
DecompressionFailed(String),
CompressionFailed(String),
}
impl fmt::Display for ConversionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConversionError::Io(e) => write!(f, "IO error: {}", e),
ConversionError::InvalidFormat(s) => write!(f, "Invalid format: {}", s),
ConversionError::UnsupportedVersion(s) => write!(f, "Unsupported version: {}", s),
ConversionError::NbtError(s) => write!(f, "NBT error: {}", s),
ConversionError::MissingData(s) => write!(f, "Missing data: {}", s),
ConversionError::DecompressionFailed(s) => write!(f, "Decompression failed: {}", s),
ConversionError::CompressionFailed(s) => write!(f, "Compression failed: {}", s),
}
}
}
impl std::error::Error for ConversionError {}
impl From<io::Error> for ConversionError {
fn from(e: io::Error) -> Self {
ConversionError::Io(e)
}
}

View file

@ -1,234 +0,0 @@
use std::collections::HashMap;
use std::io::Write;
use flate2::write::ZlibEncoder;
use flate2::Compression as FlateCompression;
use crate::console2lce::error::ConversionError;
use crate::console2lce::nbt_util::{NbtTag, write_nbt_le};
pub fn encode_legacy_nbt(
x_pos: i32,
z_pos: i32,
blocks: &[u8; 65536],
data: &[u8; 32768],
sky_light: &[u8; 32768],
_block_light: &[u8; 32768],
height_map: &[u8; 256],
biomes: &[u8; 256],
last_update: i64,
inhabited_time: i64,
entities: Vec<NbtTag>,
tile_entities: Vec<NbtTag>,
tile_ticks: Option<Vec<NbtTag>>,
) -> Result<Vec<u8>, ConversionError> {
let mut level_map = HashMap::new();
level_map.insert("xPos".to_string(), NbtTag::Int(x_pos));
level_map.insert("zPos".to_string(), NbtTag::Int(z_pos));
level_map.insert("LastUpdate".to_string(), NbtTag::Long(last_update));
level_map.insert("InhabitedTime".to_string(), NbtTag::Long(inhabited_time));
level_map.insert("Blocks".to_string(), NbtTag::ByteArray(blocks.to_vec()));
level_map.insert("Data".to_string(), NbtTag::ByteArray(data.to_vec()));
level_map.insert("SkyLight".to_string(), NbtTag::ByteArray(sky_light.to_vec()));
level_map.insert("BlockLight".to_string(), NbtTag::ByteArray(_block_light.to_vec()));
level_map.insert("HeightMap".to_string(), NbtTag::ByteArray(height_map.to_vec()));
level_map.insert("TerrainPopulatedFlags".to_string(), NbtTag::Short(0));
level_map.insert("Biomes".to_string(), NbtTag::ByteArray(biomes.to_vec()));
level_map.insert("Entities".to_string(), NbtTag::List(entities, 10));
level_map.insert("TileEntities".to_string(), NbtTag::List(tile_entities, 10));
if let Some(ticks) = tile_ticks {
level_map.insert("TileTicks".to_string(), NbtTag::List(ticks, 10));
}
let root = NbtTag::Compound(level_map);
let nbt_data = write_nbt_le(&root)?;
let rle_compressed = rle_compress(&nbt_data);
let mut encoder = ZlibEncoder::new(Vec::new(), FlateCompression::default());
encoder.write_all(&rle_compressed)?;
let zlib_data = encoder.finish()?;
Ok(zlib_data)
}
pub fn encode_compressed_storage(
x_pos: i32,
z_pos: i32,
blocks: &[u8; 65536],
data: &[u8; 32768],
sky_light: &[u8; 32768],
_block_light: &[u8; 32768],
height_map: &[u8; 256],
biomes: &[u8; 256],
last_update: i64,
) -> Result<Vec<u8>, ConversionError> {
let sections = build_compressed_tile_storage_sections(blocks, data, sky_light);
let mut level = HashMap::new();
level.insert("xPos".to_string(), NbtTag::Int(x_pos));
level.insert("zPos".to_string(), NbtTag::Int(z_pos));
level.insert("LastUpdate".to_string(), NbtTag::Long(last_update));
level.insert("Sections".to_string(), NbtTag::List(sections, 10));
level.insert("HeightMap".to_string(), NbtTag::ByteArray(height_map.to_vec()));
level.insert("Biomes".to_string(), NbtTag::ByteArray(biomes.to_vec()));
let root = NbtTag::Compound(level);
let nbt_data = write_nbt_le(&root)?;
let mut encoder = ZlibEncoder::new(Vec::new(), FlateCompression::default());
encoder.write_all(&nbt_data)?;
let zlib_data = encoder.finish()?;
Ok(zlib_data)
}
fn build_compressed_tile_storage_sections(
blocks: &[u8; 65536],
data: &[u8; 32768],
sky_light: &[u8; 32768],
) -> Vec<NbtTag> {
let mut sections = Vec::new();
for section_y in 0..8 {
let half_chunk_blocks = extract_half_chunk(blocks, section_y, 65536);
let half_chunk_data = extract_half_chunk(data, section_y, 32768);
let half_chunk_sky = extract_half_chunk(sky_light, section_y, 32768);
let storage = build_single_compressed_storage(&half_chunk_blocks, &half_chunk_data, &half_chunk_sky);
let mut section = HashMap::new();
section.insert("Y".to_string(), NbtTag::Byte(section_y as i8));
section.insert("Blocks".to_string(), NbtTag::ByteArray(storage));
sections.push(NbtTag::Compound(section));
}
sections
}
fn extract_half_chunk(full: &[u8], section_y: usize, full_size: usize) -> Vec<u8> {
let section_size = 16384;
let mut result = vec![0u8; section_size];
let base_y = section_y * 16;
for x in 0..16 {
for z in 0..16 {
for y in 0..16 {
let src_y = base_y + y;
if src_y >= 128 {
continue;
}
let src_idx = ((x * 16) + z) * 256 + src_y;
if src_idx < full_size {
let dst_idx = (y * 16 + z) * 16 + x;
if dst_idx < section_size {
result[dst_idx] = full[src_idx];
}
}
}
}
}
result
}
fn build_single_compressed_storage(blocks: &[u8], data: &[u8], _sky_light: &[u8]) -> Vec<u8> {
let mut storage = Vec::new();
let num_indices = 1024;
let mut index_table = vec![0u16; num_indices];
let mut payload = Vec::new();
for idx in 0..num_indices {
let base = idx * 16;
let mut uses_high_data = false;
let mut max_id: u8 = 0;
let mut max_data: u8 = 0;
for j in 0..16 {
let block_pos = base + j;
if block_pos >= blocks.len() {
break;
}
let b = blocks[block_pos];
let d = if block_pos < data.len() {
get_nibble(data, block_pos)
} else {
0
};
if b > max_id { max_id = b; }
if d > max_data { max_data = d; }
if d > 0 { uses_high_data = true; }
}
if max_id == 0 && max_data == 0 {
index_table[idx] = 0;
continue;
}
if max_id <= 15 && !uses_high_data {
index_table[idx] = (1 << 13) | (payload.len() as u16);
for j in 0..16 {
let block_pos = base + j;
if block_pos < blocks.len() {
let b = blocks[block_pos];
if j % 2 == 0 {
payload.push(b << 4);
} else {
let last = payload.last_mut().unwrap();
*last |= b & 0x0F;
}
}
}
continue;
}
if max_data == 0 {
index_table[idx] = (2 << 13) | (payload.len() as u16);
for j in 0..16 {
let block_pos = base + j;
if block_pos < blocks.len() {
payload.push(blocks[block_pos]);
}
}
continue;
}
if max_id > 0 || max_data > 0 {
index_table[idx] = (3 << 13) | (payload.len() as u16);
for j in 0..16 {
let block_pos = base + j;
if block_pos < blocks.len() {
payload.push(blocks[block_pos]);
if block_pos < data.len() {
payload.push(get_nibble(data, block_pos));
} else {
payload.push(0);
}
}
}
}
}
for &entry in &index_table {
storage.extend_from_slice(&entry.to_be_bytes());
}
storage.extend_from_slice(&payload);
storage
}
fn get_nibble(arr: &[u8], index: usize) -> u8 {
let b = arr[index >> 1];
if (index & 1) == 0 { b & 0x0F } else { (b >> 4) & 0x0F }
}
pub fn rle_compress(data: &[u8]) -> Vec<u8> {
let mut output = Vec::new();
let mut pos = 0;
while pos < data.len() {
let run_start = pos;
let mut run_len = 0u8;
while run_start + (run_len as usize) < data.len() && run_len < 255 {
if data[run_start] == data[run_start + (run_len as usize)] {
run_len += 1;
} else {
break;
}
}
if run_len >= 4 {
output.push(0xFF);
output.push(run_len);
output.push(data[run_start]);
pos = run_start + run_len as usize;
} else {
output.push(data[pos]);
pos += 1;
}
}
output
}

View file

@ -1,70 +0,0 @@
use std::collections::BTreeMap;
use crate::console2lce::error::ConversionError;
const SECTOR_SIZE: usize = 4096;
const HEADER_SECTORS: usize = 2;
pub struct LceRegionFile {
sectors: Vec<u8>,
offsets: BTreeMap<(i32, i32), (u32, u32, u32)>,
next_sector: u32,
}
impl LceRegionFile {
pub fn new() -> Self {
let header_size = HEADER_SECTORS * SECTOR_SIZE;
LceRegionFile {
sectors: vec![0u8; header_size],
offsets: BTreeMap::new(),
next_sector: HEADER_SECTORS as u32,
}
}
pub fn add_chunk(&mut self, chunk_x: i32, chunk_z: i32, data: &[u8]) -> Result<(), ConversionError> {
if data.len() > 1024 * 1024 {
return Err(ConversionError::InvalidFormat(format!(
"Chunk ({}, {}) data too large: {} bytes", chunk_x, chunk_z, data.len()
)));
}
let total_size = 5 + data.len();
let sectors_needed = ((total_size + SECTOR_SIZE - 1) / SECTOR_SIZE) as u32;
let sector_start = self.next_sector;
let padding = sectors_needed as usize * SECTOR_SIZE - total_size;
let length_be = (data.len() as u32).to_be_bytes();
self.sectors.extend_from_slice(&length_be);
self.sectors.push(2);
self.sectors.extend_from_slice(data);
for _ in 0..padding {
self.sectors.push(0);
}
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
self.offsets.insert((chunk_x, chunk_z), (sector_start, sectors_needed, timestamp));
self.next_sector = sector_start + sectors_needed;
Ok(())
}
pub fn build(mut self) -> Result<Vec<u8>, ConversionError> {
for ((chunk_x, chunk_z), (sector_start, sector_count, timestamp)) in &self.offsets {
let local_x = chunk_x.rem_euclid(32) as usize;
let local_z = chunk_z.rem_euclid(32) as usize;
let index = local_z * 32 + local_x;
let offset_val = (sector_start << 8) | (sector_count & 0xFF);
let offset_bytes = offset_val.to_be_bytes();
let ts_bytes = timestamp.to_be_bytes();
let off_pos = index * 4;
let ts_pos = 4096 + index * 4;
if off_pos + 4 <= self.sectors.len() {
self.sectors[off_pos..off_pos + 4].copy_from_slice(&offset_bytes);
}
if ts_pos + 4 <= self.sectors.len() {
self.sectors[ts_pos..ts_pos + 4].copy_from_slice(&ts_bytes);
}
}
Ok(self.sectors)
}
}

View file

@ -1,115 +0,0 @@
use std::collections::HashMap;
use crate::console2lce::error::ConversionError;
use crate::console2lce::nbt_util::{NbtTag, NbtHelper, write_zlibbed_nbt};
use crate::console2lce::world_profile::{get_profile, world_center_offset};
use crate::console2lce::models::ChunkConversionContext;
pub fn convert_level_dat(
source_data: &[u8],
profile_name: &str,
game_version: &str,
_context: &mut ChunkConversionContext,
) -> Result<Vec<u8>, ConversionError> {
let (_name, root) = crate::console2lce::nbt_util::read_gzipped_nbt(source_data)?;
let data = match root.as_compound() {
Some(m) => m.clone(),
None => return Err(ConversionError::InvalidFormat("level.dat root is not a compound".to_string())),
};
let data_tag = NbtHelper::get_compound(&data, "Data")
.unwrap_or_else(|| data.clone());
let profile = get_profile(profile_name).ok_or_else(|| {
ConversionError::InvalidFormat(format!("Unknown profile: {}", profile_name))
})?;
let center_offset = world_center_offset(profile.xz_size);
let spawn_x = NbtHelper::get_int(&data_tag, "SpawnX").unwrap_or(0) as i32;
let spawn_z = NbtHelper::get_int(&data_tag, "SpawnZ").unwrap_or(0) as i32;
let new_spawn_x = clamp_to_world(spawn_x, center_offset, profile.xz_size as i32);
let new_spawn_z = clamp_to_world(spawn_z, center_offset, profile.xz_size as i32);
let game_type = NbtHelper::get_int(&data_tag, "GameType").unwrap_or(0);
let rain_time = NbtHelper::get_int(&data_tag, "rainTime").unwrap_or(0);
let thunder_time = NbtHelper::get_int(&data_tag, "thunderTime").unwrap_or(0);
let raining = NbtHelper::get_byte(&data_tag, "raining").unwrap_or(0) != 0;
let thundering = NbtHelper::get_byte(&data_tag, "thundering").unwrap_or(0) != 0;
let seed = NbtHelper::get_long(&data_tag, "RandomSeed").unwrap_or(0);
let level_name = NbtHelper::get_string(&data_tag, "LevelName").unwrap_or_else(|| "Imported World".to_string());
let day_time = NbtHelper::get_int(&data_tag, "DayTime").unwrap_or(0);
let allow_commands = NbtHelper::get_byte(&data_tag, "allowCommands").unwrap_or(0) != 0;
let difficulty = NbtHelper::get_byte(&data_tag, "Difficulty").unwrap_or(2);
let difficulty_locked = NbtHelper::get_byte(&data_tag, "DifficultyLocked").unwrap_or(0) != 0;
let hardcore = NbtHelper::get_byte(&data_tag, "hardcore").unwrap_or(0) != 0;
let map_features = NbtHelper::get_byte(&data_tag, "MapFeatures").unwrap_or(1) != 0;
let generator_name = NbtHelper::get_string(&data_tag, "generatorName").unwrap_or_else(|| "default".to_string());
let generator_version = NbtHelper::get_int(&data_tag, "generatorVersion").unwrap_or(1);
let generator_options = NbtHelper::get_string(&data_tag, "generatorOptions").unwrap_or_default();
let initialized = NbtHelper::get_byte(&data_tag, "initialized").unwrap_or(1) != 0;
let clear_weather = rained_since_last_thunder_cleanup(&data_tag);
let mut game_rules = HashMap::new();
if let Some(rules) = NbtHelper::get_compound(&data_tag, "GameRules") {
for (k, v) in &rules {
if let Some(s) = v.get_string() {
game_rules.insert(k.clone(), NbtTag::String(s.to_string()));
}
}
}
let mut lce_data = HashMap::new();
lce_data.insert("allowCommands".to_string(), NbtTag::Byte(if allow_commands { 1 } else { 0 }));
lce_data.insert("clearedWeather".to_string(), NbtTag::Byte(if clear_weather { 1 } else { 0 }));
lce_data.insert("clearWeatherTime".to_string(), NbtTag::Int(0));
lce_data.insert("currentSaveVersion".to_string(), NbtTag::Int(9));
lce_data.insert("DayTime".to_string(), NbtTag::Int(day_time));
lce_data.insert("Difficulty".to_string(), NbtTag::Byte(difficulty));
lce_data.insert("DifficultyLocked".to_string(), NbtTag::Byte(if difficulty_locked { 1 } else { 0 }));
lce_data.insert("GameType".to_string(), NbtTag::Int(game_type));
lce_data.insert("generatorName".to_string(), NbtTag::String(generator_name));
lce_data.insert("generatorVersion".to_string(), NbtTag::Int(generator_version));
lce_data.insert("generatorOptions".to_string(), NbtTag::String(generator_options));
lce_data.insert("hardcore".to_string(), NbtTag::Byte(if hardcore { 1 } else { 0 }));
lce_data.insert("HellScale".to_string(), NbtTag::Int(profile.hell_scale as i32));
lce_data.insert("initialized".to_string(), NbtTag::Byte(if initialized { 1 } else { 0 }));
lce_data.insert("LevelName".to_string(), NbtTag::String(level_name));
lce_data.insert("MapFeatures".to_string(), NbtTag::Byte(if map_features { 1 } else { 0 }));
lce_data.insert("originalSaveVersion".to_string(), NbtTag::Int(7));
lce_data.insert("rainTime".to_string(), NbtTag::Int(rain_time));
lce_data.insert("RandomSeed".to_string(), NbtTag::Long(seed));
lce_data.insert("SpawnX".to_string(), NbtTag::Int(new_spawn_x));
lce_data.insert("SpawnZ".to_string(), NbtTag::Int(new_spawn_z));
lce_data.insert("thunderTime".to_string(), NbtTag::Int(thunder_time));
lce_data.insert("XzSize".to_string(), NbtTag::Int(profile.xz_size as i32));
lce_data.insert("raining".to_string(), NbtTag::Byte(if raining { 1 } else { 0 }));
lce_data.insert("thundering".to_string(), NbtTag::Byte(if thundering { 1 } else { 0 }));
if !game_rules.is_empty() {
lce_data.insert("GameRules".to_string(), NbtTag::Compound(game_rules));
}
let version_info = build_version_info(game_version);
lce_data.insert("Version".to_string(), NbtTag::Compound(version_info));
let root = NbtTag::Compound({
let mut m = HashMap::new();
m.insert("Data".to_string(), NbtTag::Compound(lce_data));
m
});
write_zlibbed_nbt("", &root)
}
fn clamp_to_world(value: i32, center: i32, xz_size: i32) -> i32 {
let half = xz_size * 8;
value.clamp(center - half, center + half - 1)
}
fn rained_since_last_thunder_cleanup(data: &HashMap<String, NbtTag>) -> bool {
let rain_time = NbtHelper::get_int(data, "rainTime").unwrap_or(0);
let thunder_time = NbtHelper::get_int(data, "thunderTime").unwrap_or(0);
rain_time > 0 && thunder_time == 0
}
fn build_version_info(game_version: &str) -> HashMap<String, NbtTag> {
let mut version = HashMap::new();
version.insert("Id".to_string(), NbtTag::Int(19133));
version.insert("Name".to_string(), NbtTag::String(game_version.to_string()));
version.insert("Snapshot".to_string(), NbtTag::Byte(0));
version
}

View file

@ -1,19 +0,0 @@
pub mod error;
pub mod models;
pub mod world_profile;
pub mod nbt_util;
pub mod stfs;
pub mod archive;
pub mod region;
pub mod chunk;
pub mod block_mapping;
pub mod modern_chunk_writer;
pub mod lce_chunk_payload;
pub mod lce_region;
pub mod level_dat;
pub mod save_data_container;
pub mod convert;
pub use error::ConversionError;
pub use models::{ConversionOptions, ConversionResult, ChunkConversionContext};
pub use world_profile::WorldProfile;
pub use convert::{convert_java_world_to_lce, convert_xbox360_save_to_lce};

View file

@ -1,129 +0,0 @@
use std::collections::HashMap;
#[derive(Debug, Clone, Copy)]
pub struct LegacyBlockState {
pub id: u8,
pub data: u8,
}
impl LegacyBlockState {
pub fn new(id: u8, data: u8) -> Self {
LegacyBlockState { id, data }
}
}
#[derive(Debug, Clone)]
pub struct ChunkConversionContext {
pub preserve_dynamic_chunk_data: bool,
pub global_modern_section_shift: Option<i32>,
pub unknown_blocks: Vec<String>,
}
impl ChunkConversionContext {
pub fn new() -> Self {
ChunkConversionContext {
preserve_dynamic_chunk_data: false,
global_modern_section_shift: None,
unknown_blocks: Vec::new(),
}
}
pub fn record_unknown_modern_block(&mut self, name: &str) {
if !self.unknown_blocks.iter().any(|s| s == name) {
self.unknown_blocks.push(name.to_string());
}
}
}
#[derive(Debug, Clone)]
pub struct ConversionOptions {
pub profile: String,
pub target_version: String,
pub game_version: String,
pub preserve_entities: bool,
pub preview: bool,
}
impl Default for ConversionOptions {
fn default() -> Self {
ConversionOptions {
profile: "large".to_string(),
target_version: "TU19".to_string(),
game_version: "1.13".to_string(),
preserve_entities: false,
preview: false,
}
}
}
#[derive(Debug, Clone)]
pub struct ConversionResult {
pub success: bool,
pub message: String,
pub chunk_count: usize,
pub unknown_blocks: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct PreparedJavaWorld {
pub level_dat: HashMap<String, Vec<u8>>,
pub region_files: Vec<JavaRegionFile>,
pub dimension_dirs: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct JavaRegionFile {
pub path: String,
pub dimension: String,
pub data: Vec<u8>,
pub chunk_count: usize,
}
#[derive(Debug, Clone)]
pub struct ArchiveEntry {
pub name: String,
pub offset: u64,
pub length: u32,
pub timestamp: u64,
}
#[derive(Debug, Clone)]
pub struct StfsPackageDescriptor {
pub magic: u32,
pub content_type: u32,
pub meta_data_size: u32,
pub total_block_count: u32,
pub block_allocation_table_size: u32,
}
#[derive(Debug, Clone)]
pub struct SavegameEnvelope {
pub data: Vec<u8>,
pub decompressed_size: u32,
pub compression_type: String,
}
pub struct DecodedSection {
pub section_y: i32,
pub blocks: [u8; 4096],
pub data: [u8; 2048],
pub sky_light: Option<[u8; 2048]>,
pub block_light: Option<[u8; 2048]>,
pub non_air_count: i32,
}
pub const CHUNK_BLOCKS: usize = 65536;
pub const CHUNK_NIBBLES: usize = 32768;
pub const HEIGHTMAP_SIZE: usize = 256;
pub const BIOMES_SIZE: usize = 256;
pub struct SaveDataContainerHeader {
pub magic: [u8; 6],
pub version: u32,
pub file_count: u32,
}
pub struct SaveDataContainerFileEntry {
pub offset: u64,
pub length: u64,
pub timestamp: u64,
pub name: String,
}

View file

@ -1,737 +0,0 @@
use std::collections::HashMap;
use crate::console2lce::models::{LegacyBlockState, ChunkConversionContext, CHUNK_BLOCKS, CHUNK_NIBBLES};
use crate::console2lce::nbt_util::{NbtTag, NbtHelper};
use crate::console2lce::block_mapping;
pub fn map_modern_block_state(name: &str, properties: &HashMap<String, String>, context: &mut ChunkConversionContext) -> LegacyBlockState {
let name = name.strip_prefix("minecraft:").unwrap_or(name);
if let Some(result) = try_map_fluid_block(name, properties) {
return result;
}
if let Some(result) = try_map_slab_block(name, properties) {
return result;
}
if let Some(result) = try_map_directional_block(name, properties) {
return result;
}
if let Some(result) = try_map_flattened_colored_block(name) {
return result;
}
if let Some(legacy) = block_mapping::MODERN_DIRECT_MAP.get(name) {
return *legacy;
}
if let Some(result) = try_map_colored_block(name, properties) {
return result;
}
if let Some(result) = try_map_wood_block(name, properties) {
return result;
}
if let Some(result) = try_map_variant_block(name, properties) {
return result;
}
context.record_unknown_modern_block(name);
LegacyBlockState::new(0, 0)
}
fn get_prop<'a>(properties: &'a HashMap<String, String>, key: &str) -> &'a str {
properties.get(key).map(|s| s.as_str()).unwrap_or("")
}
fn get_bool_prop(properties: &HashMap<String, String>, key: &str) -> bool {
get_prop(properties, key) == "true"
}
fn get_int_prop(properties: &HashMap<String, String>, key: &str, default: i32) -> i32 {
get_prop(properties, key).parse::<i32>().unwrap_or(default)
}
fn try_map_fluid_block(name: &str, properties: &HashMap<String, String>) -> Option<LegacyBlockState> {
if name != "water" && name != "lava" {
return None;
}
let level = get_int_prop(properties, "level", 0).clamp(0, 15);
let is_source = level == 0;
if name == "water" {
Some(LegacyBlockState::new(if is_source { 9 } else { 8 }, level as u8))
} else {
Some(LegacyBlockState::new(if is_source { 11 } else { 10 }, level as u8))
}
}
fn try_map_slab_block(name: &str, properties: &HashMap<String, String>) -> Option<LegacyBlockState> {
if !name.ends_with("_slab") {
return None;
}
let slab_type = get_prop(properties, "type");
let is_top = slab_type == "top";
let is_double = slab_type == "double";
if let Some(variant) = get_wood_slab_variant(name) {
if is_double {
return Some(LegacyBlockState::new(125, variant));
}
return Some(LegacyBlockState::new(126, variant | if is_top { 8 } else { 0 }));
}
if let Some(variant) = get_stone_slab_variant(name) {
if is_double {
return Some(LegacyBlockState::new(43, variant));
}
return Some(LegacyBlockState::new(44, variant | if is_top { 8 } else { 0 }));
}
if is_double {
Some(LegacyBlockState::new(43, 0))
} else {
Some(LegacyBlockState::new(44, if is_top { 8 } else { 0 }))
}
}
fn get_wood_slab_variant(name: &str) -> Option<u8> {
match name {
"oak_slab" => Some(0),
"spruce_slab" => Some(1),
"birch_slab" => Some(2),
"jungle_slab" => Some(3),
"acacia_slab" => Some(4),
"dark_oak_slab" => Some(5),
_ => None,
}
}
fn get_stone_slab_variant(name: &str) -> Option<u8> {
match name {
"stone_slab" | "smooth_stone_slab" | "andesite_slab" | "polished_andesite_slab"
| "diorite_slab" | "polished_diorite_slab" | "granite_slab" | "polished_granite_slab" => Some(0),
"sandstone_slab" | "smooth_sandstone_slab" | "cut_sandstone_slab"
| "red_sandstone_slab" | "smooth_red_sandstone_slab" | "cut_red_sandstone_slab" => Some(1),
"cobblestone_slab" | "mossy_cobblestone_slab" => Some(3),
"brick_slab" => Some(4),
"stone_brick_slab" | "mossy_stone_brick_slab" => Some(5),
"nether_brick_slab" | "red_nether_brick_slab" => Some(6),
"quartz_slab" | "smooth_quartz_slab" | "purpur_slab"
| "prismarine_slab" | "prismarine_brick_slab" | "dark_prismarine_slab" => Some(7),
_ => None,
}
}
fn try_map_directional_block(name: &str, properties: &HashMap<String, String>) -> Option<LegacyBlockState> {
match name {
"ladder" => {
let data = map_ladder_facing(get_prop(properties, "facing"));
Some(LegacyBlockState::new(65, data))
}
"vine" => {
let data = map_vine_faces(properties);
Some(LegacyBlockState::new(106, data))
}
"lever" => {
let mut data = map_lever_data(get_prop(properties, "face"), get_prop(properties, "facing"));
if get_bool_prop(properties, "powered") { data |= 8; }
Some(LegacyBlockState::new(69, data))
}
_ => {
if name.ends_with("_button") {
let id = if is_wood_family(name) { 143 } else { 77 };
let mut data = map_button_facing(get_prop(properties, "facing"));
if get_bool_prop(properties, "powered") { data |= 8; }
return Some(LegacyBlockState::new(id, data));
}
if name.ends_with("_fence_gate") {
let mut data = map_fence_gate_facing(get_prop(properties, "facing"));
if get_bool_prop(properties, "open") { data |= 4; }
return Some(LegacyBlockState::new(107, data));
}
if name.ends_with("_pressure_plate") {
let id = match name {
"light_weighted_pressure_plate" => 147,
"heavy_weighted_pressure_plate" => 148,
n if is_wood_family(n) => 72,
_ => 70,
};
let powered = get_bool_prop(properties, "powered") || get_int_prop(properties, "power", 0) > 0;
return Some(LegacyBlockState::new(id, if powered { 1 } else { 0 }));
}
if name.ends_with("_door") || name == "iron_door" {
let id = if name == "iron_door" { 71 } else { 64 };
let half = get_prop(properties, "half");
if half == "upper" {
let mut data: u8 = 8;
if get_prop(properties, "hinge") == "right" { data |= 1; }
if get_bool_prop(properties, "powered") { data |= 2; }
return Some(LegacyBlockState::new(id, data));
}
let mut data = map_door_facing(get_prop(properties, "facing"));
if get_bool_prop(properties, "open") { data |= 4; }
return Some(LegacyBlockState::new(id, data));
}
if let Some(stairs_id) = get_stairs_id(name) {
let mut data = map_stairs_facing(get_prop(properties, "facing"));
if get_prop(properties, "half") == "top" { data |= 4; }
return Some(LegacyBlockState::new(stairs_id, data));
}
if name.ends_with("trapdoor") {
let mut data = map_trapdoor_facing(get_prop(properties, "facing"));
if get_bool_prop(properties, "open") { data |= 4; }
if get_prop(properties, "half") == "top" { data |= 8; }
return Some(LegacyBlockState::new(96, data));
}
if name == "dispenser" || name == "dropper" {
let id = if name == "dropper" { 158 } else { 23 };
let mut data = map_facing_data(get_prop(properties, "facing"));
if get_bool_prop(properties, "triggered") { data |= 8; }
return Some(LegacyBlockState::new(id, data));
}
if name == "piston" || name == "sticky_piston" {
let id = if name == "sticky_piston" { 29 } else { 33 };
let mut data = map_facing_data(get_prop(properties, "facing"));
if get_bool_prop(properties, "extended") { data |= 8; }
return Some(LegacyBlockState::new(id, data));
}
if name == "piston_head" {
let mut data = map_facing_data(get_prop(properties, "facing"));
if get_prop(properties, "type") == "sticky" { data |= 8; }
return Some(LegacyBlockState::new(34, data));
}
if name == "redstone_wire" {
let data = get_int_prop(properties, "power", 0).clamp(0, 15) as u8;
return Some(LegacyBlockState::new(55, data));
}
if name == "repeater" {
let id = if get_bool_prop(properties, "powered") { 94 } else { 93 };
let dir = map_repeater_direction(get_prop(properties, "facing"));
let delay = get_int_prop(properties, "delay", 1).clamp(1, 4);
let data = dir | ((delay - 1) as u8) << 2;
return Some(LegacyBlockState::new(id, data));
}
if name == "comparator" {
let id = if get_bool_prop(properties, "powered") { 150 } else { 149 };
let dir = map_repeater_direction(get_prop(properties, "facing"));
let mut data = dir;
if get_prop(properties, "mode") == "subtract" { data |= 4; }
return Some(LegacyBlockState::new(id, data));
}
if name == "wall_torch" {
let data = map_wall_torch_facing(get_prop(properties, "facing"));
return Some(LegacyBlockState::new(50, data));
}
if name == "redstone_wall_torch" {
let id = if get_bool_prop(properties, "lit") { 76 } else { 75 };
let data = map_wall_torch_facing(get_prop(properties, "facing"));
return Some(LegacyBlockState::new(id, data));
}
if name == "nether_wart" {
let data = get_int_prop(properties, "age", 0).clamp(0, 3) as u8;
return Some(LegacyBlockState::new(115, data));
}
if name == "pumpkin_stem" || name == "attached_pumpkin_stem" {
let data = if name == "attached_pumpkin_stem" { 7 } else { get_int_prop(properties, "age", 0).clamp(0, 7) as u8 };
return Some(LegacyBlockState::new(104, data));
}
if name == "melon_stem" || name == "attached_melon_stem" {
let data = if name == "attached_melon_stem" { 7 } else { get_int_prop(properties, "age", 0).clamp(0, 7) as u8 };
return Some(LegacyBlockState::new(105, data));
}
if name == "cocoa" {
let age = get_int_prop(properties, "age", 0).clamp(0, 2) as u8;
let dir = map_repeater_direction(get_prop(properties, "facing"));
return Some(LegacyBlockState::new(127, (age << 2) | dir));
}
if name == "hay_block" {
let data = match get_prop(properties, "axis") {
"x" => 4,
"z" => 8,
_ => 0,
};
return Some(LegacyBlockState::new(170, data));
}
if name == "quartz_pillar" {
let data = match get_prop(properties, "axis") {
"x" => 3,
"z" => 4,
_ => 2,
};
return Some(LegacyBlockState::new(155, data));
}
if name == "nether_portal" {
let data = if get_prop(properties, "axis") == "z" { 2 } else { 1 };
return Some(LegacyBlockState::new(90, data));
}
None
}
}
}
fn is_wood_family(name: &str) -> bool {
let woods = ["oak", "spruce", "birch", "jungle", "acacia", "dark_oak"];
woods.iter().any(|w| name.starts_with(w))
}
fn get_stairs_id(name: &str) -> Option<u8> {
match name {
"oak_stairs" | "spruce_stairs" | "birch_stairs" | "jungle_stairs"
| "acacia_stairs" | "dark_oak_stairs" => Some(53),
"cobblestone_stairs" | "mossy_cobblestone_stairs" | "stone_stairs" => Some(67),
"brick_stairs" => Some(108),
"stone_brick_stairs" | "mossy_stone_brick_stairs" => Some(109),
"nether_brick_stairs" => Some(114),
"sandstone_stairs" | "red_sandstone_stairs" => Some(128),
"quartz_stairs" | "purpur_stairs" => Some(156),
_ => None,
}
}
fn map_stairs_facing(facing: &str) -> u8 {
match facing {
"east" => 0, "west" => 1, "south" => 2, "north" => 3, _ => 0,
}
}
fn map_ladder_facing(facing: &str) -> u8 {
match facing {
"north" => 2, "south" => 3, "west" => 4, "east" => 5, _ => 2,
}
}
fn map_button_facing(facing: &str) -> u8 {
match facing {
"east" => 1, "west" => 2, "south" => 3, "north" => 4, _ => 1,
}
}
fn map_fence_gate_facing(facing: &str) -> u8 {
match facing {
"south" => 0, "west" => 1, "north" => 2, "east" => 3, _ => 0,
}
}
fn map_vine_faces(properties: &HashMap<String, String>) -> u8 {
let mut data = 0u8;
if get_bool_prop(properties, "south") { data |= 1; }
if get_bool_prop(properties, "west") { data |= 2; }
if get_bool_prop(properties, "north") { data |= 4; }
if get_bool_prop(properties, "east") { data |= 8; }
data
}
fn map_lever_data(face: &str, facing: &str) -> u8 {
match face {
"floor" => if facing == "east" || facing == "west" { 6 } else { 5 },
"ceiling" => if facing == "east" || facing == "west" { 7 } else { 0 },
_ => map_button_facing(facing),
}
}
fn map_door_facing(facing: &str) -> u8 {
match facing {
"east" => 0, "south" => 1, "west" => 2, "north" => 3, _ => 0,
}
}
fn map_trapdoor_facing(facing: &str) -> u8 {
match facing {
"north" => 0, "south" => 1, "west" => 2, "east" => 3, _ => 0,
}
}
fn map_facing_data(facing: &str) -> u8 {
match facing {
"down" => 0, "up" => 1, "north" => 2, "south" => 3, "west" => 4, "east" => 5, _ => 3,
}
}
fn map_repeater_direction(facing: &str) -> u8 {
match facing {
"south" => 0, "west" => 1, "north" => 2, "east" => 3, _ => 0,
}
}
fn map_wall_torch_facing(facing: &str) -> u8 {
match facing {
"east" => 1, "west" => 2, "south" => 3, "north" => 4, _ => 1,
}
}
fn try_map_colored_block(name: &str, properties: &HashMap<String, String>) -> Option<LegacyBlockState> {
let color = get_prop(properties, "color");
let color_data = block_mapping::get_color_data(color);
match name {
"wool" => Some(LegacyBlockState::new(35, color_data)),
"stained_glass" => Some(LegacyBlockState::new(95, color_data)),
"stained_glass_pane" => Some(LegacyBlockState::new(160, color_data)),
"terracotta" | "stained_hardened_clay" => Some(LegacyBlockState::new(159, color_data)),
"concrete" => Some(LegacyBlockState::new(172, color_data)),
"concrete_powder" => Some(LegacyBlockState::new(12, color_data)),
"glazed_terracotta" => Some(LegacyBlockState::new(159, color_data)),
_ => None,
}
}
fn try_map_flattened_colored_block(name: &str) -> Option<LegacyBlockState> {
let (color_data, suffix) = split_color_prefix(name)?;
match suffix {
"wool" => Some(LegacyBlockState::new(35, color_data)),
"stained_glass" | "glass" => Some(LegacyBlockState::new(95, color_data)),
"stained_glass_pane" | "glass_pane" => Some(LegacyBlockState::new(160, color_data)),
"terracotta" | "stained_hardened_clay" => Some(LegacyBlockState::new(159, color_data)),
"concrete" => Some(LegacyBlockState::new(172, color_data)),
"concrete_powder" => Some(LegacyBlockState::new(12, color_data)),
"glazed_terracotta" => Some(LegacyBlockState::new(159, color_data)),
"carpet" => Some(LegacyBlockState::new(171, color_data)),
_ => None,
}
}
fn split_color_prefix(name: &str) -> Option<(u8, &str)> {
for color_name in block_mapping::COLOR_NAMES {
if let Some(suffix) = name.strip_prefix(color_name).and_then(|s| s.strip_prefix('_')) {
let color_data = block_mapping::get_color_name(color_name);
return Some((color_data, suffix));
}
}
None
}
fn try_map_wood_block(name: &str, properties: &HashMap<String, String>) -> Option<LegacyBlockState> {
let wood_type = get_prop(properties, "variant");
let wood_type = if wood_type.is_empty() {
get_prefix_before_underscore(name)
} else {
wood_type.to_string()
};
let data = block_mapping::get_wood_data(&wood_type);
if name.ends_with("_planks") || name == "planks" {
return Some(LegacyBlockState::new(5, data));
}
if name.ends_with("_sapling") || name == "sapling" {
return Some(LegacyBlockState::new(6, data));
}
if name.ends_with("_log") || name == "log" {
return Some(LegacyBlockState::new(17, if data > 3 { 3 } else { data }));
}
if name.ends_with("_leaves") || name == "leaves" {
return Some(LegacyBlockState::new(18, if data > 3 { 3 } else { data }));
}
if name.ends_with("_stairs") && name.contains("wood") {
return Some(LegacyBlockState::new(53, 0));
}
if name.ends_with("_door") {
return Some(LegacyBlockState::new(64, 0));
}
if name.ends_with("_fence") {
return Some(LegacyBlockState::new(85, 0));
}
if name.ends_with("_fence_gate") {
return Some(LegacyBlockState::new(107, 0));
}
if name.ends_with("_pressure_plate") && is_wood_family(name) {
return Some(LegacyBlockState::new(72, 0));
}
None
}
fn get_prefix_before_underscore(name: &str) -> String {
if let Some(idx) = name.find('_') {
name[..idx].to_string()
} else {
String::new()
}
}
fn try_map_variant_block(name: &str, properties: &HashMap<String, String>) -> Option<LegacyBlockState> {
match name {
"redstone_lamp" => {
let lit = get_bool_prop(properties, "lit");
Some(LegacyBlockState::new(if lit { 124 } else { 123 }, 0))
}
"deepslate" | "polished_deepslate" | "tuff" | "calcite" | "dripstone_block" => Some(LegacyBlockState::new(1, 0)),
"cobbled_deepslate" => Some(LegacyBlockState::new(4, 0)),
"deepslate_bricks" | "deepslate_tiles" => Some(LegacyBlockState::new(98, 0)),
"cracked_deepslate_bricks" | "cracked_deepslate_tiles" => Some(LegacyBlockState::new(98, 2)),
"chiseled_deepslate" => Some(LegacyBlockState::new(98, 3)),
"deepslate_coal_ore" => Some(LegacyBlockState::new(16, 0)),
"deepslate_iron_ore" | "deepslate_copper_ore" => Some(LegacyBlockState::new(15, 0)),
"deepslate_gold_ore" => Some(LegacyBlockState::new(14, 0)),
"deepslate_redstone_ore" => Some(LegacyBlockState::new(73, 0)),
"deepslate_lapis_ore" => Some(LegacyBlockState::new(21, 0)),
"deepslate_diamond_ore" => Some(LegacyBlockState::new(56, 0)),
"deepslate_emerald_ore" => Some(LegacyBlockState::new(129, 0)),
"cobblestone_wall" => Some(LegacyBlockState::new(139, 0)),
"mossy_cobblestone_wall" => Some(LegacyBlockState::new(139, 1)),
"mossy_stone_bricks" => Some(LegacyBlockState::new(98, 1)),
"cracked_stone_bricks" => Some(LegacyBlockState::new(98, 2)),
"chiseled_stone_bricks" => Some(LegacyBlockState::new(98, 3)),
"smooth_sandstone" => Some(LegacyBlockState::new(24, 2)),
"chiseled_sandstone" | "cut_sandstone" => Some(LegacyBlockState::new(24, 1)),
"quartz_block" => Some(LegacyBlockState::new(155, 0)),
"chiseled_quartz_block" => Some(LegacyBlockState::new(155, 1)),
"prismarine" => Some(LegacyBlockState::new(168, 0)),
"prismarine_bricks" => Some(LegacyBlockState::new(168, 1)),
"dark_prismarine" => Some(LegacyBlockState::new(168, 2)),
"poppy" | "sunflower" | "lilac" | "peony" | "rose_bush" => Some(LegacyBlockState::new(38, 0)),
"blue_orchid" => Some(LegacyBlockState::new(38, 1)),
"allium" => Some(LegacyBlockState::new(38, 2)),
"azure_bluet" => Some(LegacyBlockState::new(38, 3)),
"red_tulip" => Some(LegacyBlockState::new(38, 4)),
"orange_tulip" => Some(LegacyBlockState::new(38, 5)),
"white_tulip" => Some(LegacyBlockState::new(38, 6)),
"pink_tulip" => Some(LegacyBlockState::new(38, 7)),
"oxeye_daisy" => Some(LegacyBlockState::new(38, 8)),
_ => None,
}
}
pub fn get_nibble(arr: &[u8], index: usize) -> u8 {
let b = arr[index >> 1];
if (index & 1) == 0 { b & 0x0F } else { (b >> 4) & 0x0F }
}
pub fn set_nibble(arr: &mut [u8], index: usize, value: u8) {
let i = index >> 1;
let value = value & 0x0F;
if (index & 1) == 0 {
arr[i] = (arr[i] & 0xF0) | value;
} else {
arr[i] = (arr[i] & 0x0F) | (value << 4);
}
}
pub fn flatten_anvil_sections(
level: &HashMap<String, NbtTag>,
section_shift: Option<i32>,
context: &mut ChunkConversionContext,
) -> ([u8; CHUNK_BLOCKS], [u8; CHUNK_NIBBLES], [u8; CHUNK_NIBBLES], [u8; CHUNK_NIBBLES]) {
let mut blocks = [0u8; CHUNK_BLOCKS];
let mut data = [0u8; CHUNK_NIBBLES];
let mut sky_light = [0xFFu8; CHUNK_NIBBLES];
let mut block_light = [0u8; CHUNK_NIBBLES];
let sections = NbtHelper::get_list(level, "Sections")
.or_else(|| NbtHelper::get_list(level, "sections"));
let sections = match sections {
Some(s) => s,
None => return (blocks, data, sky_light, block_light),
};
struct DecodedSectionData {
section_y: i32,
sblocks: [u8; 4096],
sdata: [u8; 2048],
sky: Option<[u8; 2048]>,
block: Option<[u8; 2048]>,
non_air: i32,
}
let mut decoded = Vec::new();
for section_tag in &sections {
let section = match section_tag.as_compound() {
Some(s) => s,
None => continue,
};
let section_y = NbtHelper::get_byte(section, "Y")
.or_else(|| NbtHelper::get_int(section, "y").map(|v| v as i8))
.unwrap_or(0) as i32;
let (sblocks, sdata) = match try_decode_section_blocks(section, context) {
Some(r) => r,
None => continue,
};
let s_sky = NbtHelper::get_byte_array(section, "SkyLight")
.or_else(|| NbtHelper::get_byte_array(section, "sky_light"))
.map(|v| {
let mut arr = [0u8; 2048];
let len = v.len().min(2048);
arr[..len].copy_from_slice(&v[..len]);
arr
});
let s_block = NbtHelper::get_byte_array(section, "BlockLight")
.or_else(|| NbtHelper::get_byte_array(section, "block_light"))
.map(|v| {
let mut arr = [0u8; 2048];
let len = v.len().min(2048);
arr[..len].copy_from_slice(&v[..len]);
arr
});
let non_air = sblocks.iter().filter(|&&b| b != 0).count() as i32;
decoded.push(DecodedSectionData {
section_y,
sblocks,
sdata,
sky: s_sky,
block: s_block,
non_air,
});
}
if decoded.is_empty() {
return (blocks, data, sky_light, block_light);
}
let mut effective_shift = section_shift.unwrap_or(0);
let uses_negative_y = decoded.iter().any(|s| s.section_y < 0);
if effective_shift == 0 && section_shift.is_none() {
let anchor = decoded.iter()
.max_by(|a, b| a.non_air.cmp(&b.non_air)
.then_with(|| (a.section_y - 4).abs().cmp(&(b.section_y - 4).abs())))
.map(|s| s.section_y)
.unwrap_or(4);
effective_shift = if uses_negative_y { anchor + 4 } else { anchor - 4 };
}
for section in &decoded {
let remapped_y = section.section_y - effective_shift;
if remapped_y < 0 || remapped_y > 15 {
continue;
}
let base_y = remapped_y as usize * 16;
for i in 0..4096 {
let x = i & 0x0F;
let z = (i >> 4) & 0x0F;
let y = (i >> 8) & 0x0F;
let global_y = base_y + y;
let flat_index = ((x * 16) + z) * 256 + global_y;
blocks[flat_index] = section.sblocks[i];
set_nibble(&mut data, flat_index, get_nibble(&section.sdata, i));
if let Some(ref sk) = section.sky {
set_nibble(&mut sky_light, flat_index, get_nibble(sk, i));
}
if let Some(ref bl) = section.block {
set_nibble(&mut block_light, flat_index, get_nibble(bl, i));
}
}
}
(blocks, data, sky_light, block_light)
}
fn try_decode_section_blocks(section: &HashMap<String, NbtTag>, context: &mut ChunkConversionContext) -> Option<([u8; 4096], [u8; 2048])> {
let old_blocks = NbtHelper::get_byte_array(section, "Blocks");
if let Some(ref old_b) = old_blocks {
if old_b.len() >= 4096 {
let mut blocks = [0u8; 4096];
blocks.copy_from_slice(&old_b[..4096]);
let data_arr = NbtHelper::get_byte_array_or_default(section, "Data", 2048);
let mut data = [0u8; 2048];
let len = data_arr.len().min(2048);
data[..len].copy_from_slice(&data_arr[..len]);
return Some((blocks, data));
}
}
try_decode_palette_section(section, context)
}
fn try_decode_palette_section(section: &HashMap<String, NbtTag>, context: &mut ChunkConversionContext) -> Option<([u8; 4096], [u8; 2048])> {
let block_states_container = NbtHelper::get_compound(section, "block_states");
let palette = NbtHelper::get_list(section, "Palette")
.or_else(|| block_states_container.as_ref().and_then(|c| NbtHelper::get_list(c, "palette")))?;
if palette.is_empty() {
return None;
}
let mut blocks = [0u8; 4096];
let mut data = [0u8; 2048];
if palette.len() == 1 {
let entry = match &palette[0] {
NbtTag::Compound(m) => m,
_ => return None,
};
let legacy = map_palette_entry(entry, context);
for b in blocks.iter_mut() {
*b = legacy.id;
}
if legacy.data != 0 {
for i in 0..4096 {
set_nibble(&mut data, i, legacy.data);
}
}
return Some((blocks, data));
}
let block_states = section.get("BlockStates")
.and_then(|t| t.get_long_array())
.or_else(|| block_states_container.as_ref().and_then(|c| c.get("data").and_then(|t| t.get_long_array())))?;
if block_states.is_empty() {
return None;
}
let bits_per_block = std::cmp::max(4, bits_required(palette.len() - 1));
let values_per_long = std::cmp::max(1, 64 / bits_per_block);
let expected_long_count = (4096 + values_per_long - 1) / values_per_long;
let use_padded = block_states.len() == expected_long_count as usize;
for i in 0..4096 {
let palette_index = if use_padded {
read_padded_block_state(block_states, bits_per_block, i)
} else {
read_compact_block_state(block_states, bits_per_block, i)
};
if palette_index >= palette.len() {
continue;
}
let entry = match &palette[palette_index] {
NbtTag::Compound(m) => m,
_ => continue,
};
let legacy = map_palette_entry(entry, context);
blocks[i] = legacy.id;
if legacy.data != 0 {
set_nibble(&mut data, i, legacy.data);
}
}
Some((blocks, data))
}
fn map_palette_entry(entry: &HashMap<String, NbtTag>, context: &mut ChunkConversionContext) -> LegacyBlockState {
let name = NbtHelper::get_string(entry, "Name").unwrap_or_default();
let properties = NbtHelper::get_compound(entry, "Properties").unwrap_or_default();
let mut prop_map = HashMap::new();
for (k, v) in &properties {
if let Some(s) = v.get_string() {
prop_map.insert(k.clone(), s.to_string());
}
}
map_modern_block_state(&name, &prop_map, context)
}
fn bits_required(value: usize) -> i32 {
if value == 0 { return 1; }
let mut bits = 0;
let mut v = value;
while v > 0 {
bits += 1;
v >>= 1;
}
bits
}
fn read_padded_block_state(states: &[i64], bits_per_block: i32, index: usize) -> usize {
let values_per_long = std::cmp::max(1, 64 / bits_per_block);
let long_index = index / values_per_long as usize;
let bit_offset = (index % values_per_long as usize) * bits_per_block as usize;
let mask = (1u64 << bits_per_block) - 1;
(states[long_index] as u64 >> bit_offset & mask) as usize
}
fn read_compact_block_state(states: &[i64], bits_per_block: i32, index: usize) -> usize {
let start_bit = index * bits_per_block as usize;
let long_index = start_bit >> 6;
let bit_offset = start_bit & 63;
let mask = (1u64 << bits_per_block) - 1;
let mut value = states[long_index] as u64 >> bit_offset;
let bits_read = 64 - bit_offset;
if (bits_read as usize) < bits_per_block as usize && long_index + 1 < states.len() {
value |= (states[long_index + 1] as u64) << bits_read;
}
(value & mask) as usize
}

View file

@ -1,453 +0,0 @@
use std::collections::HashMap;
use std::io::{Read, Write, Cursor};
use byteorder::{BigEndian, LittleEndian, ReadBytesExt, WriteBytesExt};
use flate2::read::GzDecoder;
use flate2::read::ZlibDecoder;
use flate2::write::ZlibEncoder;
use flate2::Compression as FlateCompression;
use crate::console2lce::error::ConversionError;
#[derive(Debug, Clone)]
pub enum NbtTag {
Byte(i8),
Short(i16),
Int(i32),
Long(i64),
Float(f32),
Double(f64),
String(String),
ByteArray(Vec<u8>),
IntArray(Vec<i32>),
LongArray(Vec<i64>),
List(Vec<NbtTag>, u8),
Compound(HashMap<String, NbtTag>),
End,
}
impl NbtTag {
pub fn get_byte(&self) -> Option<i8> {
if let NbtTag::Byte(v) = self { Some(*v) } else { None }
}
pub fn get_short(&self) -> Option<i16> {
if let NbtTag::Short(v) = self { Some(*v) } else { None }
}
pub fn get_int(&self) -> Option<i32> {
if let NbtTag::Int(v) = self { Some(*v) } else { None }
}
pub fn get_long(&self) -> Option<i64> {
if let NbtTag::Long(v) = self { Some(*v) } else { None }
}
pub fn get_float(&self) -> Option<f32> {
if let NbtTag::Float(v) = self { Some(*v) } else { None }
}
pub fn get_double(&self) -> Option<f64> {
if let NbtTag::Double(v) = self { Some(*v) } else { None }
}
pub fn get_string(&self) -> Option<&str> {
if let NbtTag::String(v) = self { Some(v.as_str()) } else { None }
}
pub fn get_byte_array(&self) -> Option<&[u8]> {
if let NbtTag::ByteArray(v) = self { Some(v.as_slice()) } else { None }
}
pub fn get_int_array(&self) -> Option<&[i32]> {
if let NbtTag::IntArray(v) = self { Some(v.as_slice()) } else { None }
}
pub fn get_long_array(&self) -> Option<&[i64]> {
if let NbtTag::LongArray(v) = self { Some(v.as_slice()) } else { None }
}
pub fn get_list(&self) -> Option<(&[NbtTag], u8)> {
if let NbtTag::List(tags, type_id) = self {
Some((tags.as_slice(), *type_id))
} else {
None
}
}
pub fn get_compound(&self) -> Option<&HashMap<String, NbtTag>> {
if let NbtTag::Compound(map) = self { Some(map) } else { None }
}
pub fn get_compound_mut(&mut self) -> Option<&mut HashMap<String, NbtTag>> {
if let NbtTag::Compound(map) = self { Some(map) } else { None }
}
pub fn as_compound(&self) -> Option<&HashMap<String, NbtTag>> {
self.get_compound()
}
pub fn lookup(&self, path: &[&str]) -> Option<&NbtTag> {
let mut current = self;
for key in path {
match current {
NbtTag::Compound(map) => {
current = map.get(*key)?;
}
_ => return None,
}
}
Some(current)
}
pub fn lookup_mut(&mut self, path: &[&str]) -> Option<&mut NbtTag> {
let mut current = self;
for key in path {
match current {
NbtTag::Compound(map) => {
current = map.get_mut(*key)?;
}
_ => return None,
}
}
Some(current)
}
}
fn read_string<R: Read>(reader: &mut R) -> Result<String, ConversionError> {
let len = reader.read_u16::<BigEndian>()? as usize;
let mut buf = vec![0u8; len];
reader.read_exact(&mut buf)?;
Ok(String::from_utf8(buf).map_err(|e| ConversionError::NbtError(format!("Invalid UTF-8: {}", e)))?)
}
fn write_string<W: Write>(writer: &mut W, s: &str) -> Result<(), ConversionError> {
let bytes = s.as_bytes();
if bytes.len() > u16::MAX as usize {
return Err(ConversionError::NbtError("String too long".to_string()));
}
writer.write_u16::<BigEndian>(bytes.len() as u16)?;
writer.write_all(bytes)?;
Ok(())
}
fn read_tag<R: Read>(reader: &mut R, tag_type: u8) -> Result<NbtTag, ConversionError> {
match tag_type {
0 => Ok(NbtTag::End),
1 => Ok(NbtTag::Byte(reader.read_i8()?)),
2 => Ok(NbtTag::Short(reader.read_i16::<BigEndian>()?)),
3 => Ok(NbtTag::Int(reader.read_i32::<BigEndian>()?)),
4 => Ok(NbtTag::Long(reader.read_i64::<BigEndian>()?)),
5 => Ok(NbtTag::Float(reader.read_f32::<BigEndian>()?)),
6 => Ok(NbtTag::Double(reader.read_f64::<BigEndian>()?)),
7 => {
let len = reader.read_i32::<BigEndian>()?;
if len < 0 {
return Err(ConversionError::NbtError("Negative byte array length".to_string()));
}
let mut buf = vec![0u8; len as usize];
reader.read_exact(&mut buf)?;
Ok(NbtTag::ByteArray(buf))
}
8 => Ok(NbtTag::String(read_string(reader)?)),
9 => {
let element_type = reader.read_u8()?;
let len = reader.read_i32::<BigEndian>()?;
if len < 0 {
return Err(ConversionError::NbtError("Negative list length".to_string()));
}
let mut tags = Vec::with_capacity(len as usize);
for _ in 0..len {
tags.push(read_tag(reader, element_type)?);
}
Ok(NbtTag::List(tags, element_type))
}
10 => {
let mut map = HashMap::new();
loop {
let item_type = reader.read_u8()?;
if item_type == 0 { break; }
let name = read_string(reader)?;
let tag = read_tag(reader, item_type)?;
map.insert(name, tag);
}
Ok(NbtTag::Compound(map))
}
11 => {
let len = reader.read_i32::<BigEndian>()?;
if len < 0 {
return Err(ConversionError::NbtError("Negative int array length".to_string()));
}
let mut buf = Vec::with_capacity(len as usize);
for _ in 0..len {
buf.push(reader.read_i32::<BigEndian>()?);
}
Ok(NbtTag::IntArray(buf))
}
12 => {
let len = reader.read_i32::<BigEndian>()?;
if len < 0 {
return Err(ConversionError::NbtError("Negative long array length".to_string()));
}
let mut buf = Vec::with_capacity(len as usize);
for _ in 0..len {
buf.push(reader.read_i64::<BigEndian>()?);
}
Ok(NbtTag::LongArray(buf))
}
_ => Err(ConversionError::NbtError(format!("Unknown tag type: {}", tag_type))),
}
}
fn write_tag<W: Write>(writer: &mut W, tag: &NbtTag) -> Result<(), ConversionError> {
match tag {
NbtTag::End => writer.write_u8(0)?,
NbtTag::Byte(v) => writer.write_i8(*v)?,
NbtTag::Short(v) => writer.write_i16::<BigEndian>(*v)?,
NbtTag::Int(v) => writer.write_i32::<BigEndian>(*v)?,
NbtTag::Long(v) => writer.write_i64::<BigEndian>(*v)?,
NbtTag::Float(v) => writer.write_f32::<BigEndian>(*v)?,
NbtTag::Double(v) => writer.write_f64::<BigEndian>(*v)?,
NbtTag::ByteArray(v) => {
writer.write_i32::<BigEndian>(v.len() as i32)?;
writer.write_all(v)?;
}
NbtTag::String(s) => write_string(writer, s)?,
NbtTag::List(tags, _) => {
let element_type = if tags.is_empty() { 0 } else { get_tag_type(&tags[0]) };
writer.write_u8(element_type)?;
writer.write_i32::<BigEndian>(tags.len() as i32)?;
for tag in tags {
write_tag(writer, tag)?;
}
}
NbtTag::Compound(map) => {
for (name, tag) in map {
let tag_type = get_tag_type(tag);
writer.write_u8(tag_type)?;
write_string(writer, name)?;
write_tag(writer, tag)?;
}
writer.write_u8(0)?;
}
NbtTag::IntArray(v) => {
writer.write_i32::<BigEndian>(v.len() as i32)?;
for val in v {
writer.write_i32::<BigEndian>(*val)?;
}
}
NbtTag::LongArray(v) => {
writer.write_i32::<BigEndian>(v.len() as i32)?;
for val in v {
writer.write_i64::<BigEndian>(*val)?;
}
}
}
Ok(())
}
fn get_tag_type(tag: &NbtTag) -> u8 {
match tag {
NbtTag::End => 0,
NbtTag::Byte(_) => 1,
NbtTag::Short(_) => 2,
NbtTag::Int(_) => 3,
NbtTag::Long(_) => 4,
NbtTag::Float(_) => 5,
NbtTag::Double(_) => 6,
NbtTag::ByteArray(_) => 7,
NbtTag::String(_) => 8,
NbtTag::List(_, _) => 9,
NbtTag::Compound(_) => 10,
NbtTag::IntArray(_) => 11,
NbtTag::LongArray(_) => 12,
}
}
pub fn read_nbt(data: &[u8]) -> Result<(String, NbtTag), ConversionError> {
let mut reader = Cursor::new(data);
let root_type = reader.read_u8()?;
if root_type == 0 {
return Ok((String::new(), NbtTag::End));
}
let name = read_string(&mut reader)?;
let tag = read_tag(&mut reader, root_type)?;
Ok((name, tag))
}
pub fn write_nbt(name: &str, tag: &NbtTag) -> Result<Vec<u8>, ConversionError> {
let mut buf = Vec::new();
let tag_type = get_tag_type(tag);
buf.write_u8(tag_type)?;
write_string(&mut buf, name)?;
write_tag(&mut buf, tag)?;
Ok(buf)
}
pub fn read_gzipped_nbt(data: &[u8]) -> Result<(String, NbtTag), ConversionError> {
let mut decoder = GzDecoder::new(data);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?;
read_nbt(&decompressed)
}
pub fn read_zlibbed_nbt(data: &[u8]) -> Result<(String, NbtTag), ConversionError> {
let mut decoder = ZlibDecoder::new(data);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?;
read_nbt(&decompressed)
}
pub fn write_zlibbed_nbt(name: &str, tag: &NbtTag) -> Result<Vec<u8>, ConversionError> {
let nbt_data = write_nbt(name, tag)?;
let mut encoder = ZlibEncoder::new(Vec::new(), FlateCompression::default());
encoder.write_all(&nbt_data)?;
let compressed = encoder.finish()?;
Ok(compressed)
}
pub fn write_nbt_le(tag: &NbtTag) -> Result<Vec<u8>, ConversionError> {
let mut buf = Vec::new();
let tag_type = get_tag_type(tag);
buf.write_u8(tag_type)?;
write_tag_le(&mut buf, tag)?;
Ok(buf)
}
fn write_tag_le<W: Write>(writer: &mut W, tag: &NbtTag) -> Result<(), ConversionError> {
match tag {
NbtTag::End => writer.write_u8(0)?,
NbtTag::Byte(v) => writer.write_i8(*v)?,
NbtTag::Short(v) => writer.write_i16::<LittleEndian>(*v)?,
NbtTag::Int(v) => writer.write_i32::<LittleEndian>(*v)?,
NbtTag::Long(v) => writer.write_i64::<LittleEndian>(*v)?,
NbtTag::Float(v) => writer.write_f32::<LittleEndian>(*v)?,
NbtTag::Double(v) => writer.write_f64::<LittleEndian>(*v)?,
NbtTag::ByteArray(v) => {
writer.write_i32::<LittleEndian>(v.len() as i32)?;
writer.write_all(v)?;
}
NbtTag::String(s) => {
let bytes = s.as_bytes();
writer.write_u16::<LittleEndian>(bytes.len() as u16)?;
writer.write_all(bytes)?;
}
NbtTag::List(tags, _) => {
let element_type = if tags.is_empty() { 0 } else { get_tag_type(&tags[0]) };
writer.write_u8(element_type)?;
writer.write_i32::<LittleEndian>(tags.len() as i32)?;
for t in tags {
write_tag_le(writer, t)?;
}
}
NbtTag::Compound(map) => {
for (name, t) in map {
let tt = get_tag_type(t);
writer.write_u8(tt)?;
let bytes = name.as_bytes();
writer.write_u16::<LittleEndian>(bytes.len() as u16)?;
writer.write_all(bytes)?;
write_tag_le(writer, t)?;
}
writer.write_u8(0)?;
}
NbtTag::IntArray(v) => {
writer.write_i32::<LittleEndian>(v.len() as i32)?;
for val in v {
writer.write_i32::<LittleEndian>(*val)?;
}
}
NbtTag::LongArray(v) => {
writer.write_i32::<LittleEndian>(v.len() as i32)?;
for val in v {
writer.write_i64::<LittleEndian>(*val)?;
}
}
}
Ok(())
}
pub fn read_mcr_chunk(data: &[u8], _compressed_size: u32) -> Result<Vec<u8>, ConversionError> {
if data.len() < 5 {
return Err(ConversionError::InvalidFormat("Chunk data too short".to_string()));
}
let compression_scheme = data[4];
let payload = &data[5..];
match compression_scheme {
1 => {
let mut decoder = GzDecoder::new(payload);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?;
Ok(decompressed)
}
2 => {
let mut decoder = ZlibDecoder::new(payload);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?;
Ok(decompressed)
}
3 => {
Ok(payload.to_vec())
}
_ => Err(ConversionError::DecompressionFailed(
format!("Unknown compression scheme: {}", compression_scheme)
)),
}
}
#[derive(Debug, Clone)]
pub struct NbtHelper;
impl NbtHelper {
pub fn get_byte_array(tag: &HashMap<String, NbtTag>, name: &str) -> Option<Vec<u8>> {
tag.get(name).and_then(|t| t.get_byte_array().map(|v| v.to_vec()))
}
pub fn get_byte_array_or_default(tag: &HashMap<String, NbtTag>, name: &str, default_size: usize) -> Vec<u8> {
Self::get_byte_array(tag, name).map(|v| {
if v.len() >= default_size {
v[..default_size].to_vec()
} else {
let mut padded = vec![0u8; default_size];
padded[..v.len()].copy_from_slice(&v);
padded
}
}).unwrap_or_else(|| vec![0u8; default_size])
}
pub fn get_int(tag: &HashMap<String, NbtTag>, name: &str) -> Option<i32> {
tag.get(name).and_then(|t| t.get_int())
}
pub fn get_long(tag: &HashMap<String, NbtTag>, name: &str) -> Option<i64> {
tag.get(name).and_then(|t| t.get_long())
}
pub fn get_short(tag: &HashMap<String, NbtTag>, name: &str) -> Option<i16> {
tag.get(name).and_then(|t| t.get_short())
}
pub fn get_byte(tag: &HashMap<String, NbtTag>, name: &str) -> Option<i8> {
tag.get(name).and_then(|t| t.get_byte())
}
pub fn get_string(tag: &HashMap<String, NbtTag>, name: &str) -> Option<String> {
tag.get(name).and_then(|t| t.get_string().map(|s| s.to_string()))
}
pub fn get_list(tag: &HashMap<String, NbtTag>, name: &str) -> Option<Vec<NbtTag>> {
tag.get(name).and_then(|t| {
if let NbtTag::List(tags, _) = t {
Some(tags.clone())
} else {
None
}
})
}
pub fn get_compound(tag: &HashMap<String, NbtTag>, name: &str) -> Option<HashMap<String, NbtTag>> {
tag.get(name).and_then(|t| {
if let NbtTag::Compound(map) = t {
Some(map.clone())
} else {
None
}
})
}
}

View file

@ -1,124 +0,0 @@
use crate::console2lce::error::ConversionError;
const SECTOR_SIZE: u32 = 4096;
const REGION_SIZE_X: usize = 32;
const REGION_SIZE_Z: usize = 32;
#[derive(Debug, Clone, Default)]
pub struct RegionEntry {
pub offset: u32,
pub sector_count: u32,
pub timestamp: u32,
pub exists: bool,
}
#[derive(Debug, Clone)]
pub struct Xbox360Region {
pub entries: [[RegionEntry; REGION_SIZE_Z]; REGION_SIZE_X],
pub raw_data: Vec<u8>,
}
pub fn parse_xbox360_region(data: &[u8]) -> Result<Xbox360Region, ConversionError> {
if data.len() < 8192 {
return Err(ConversionError::InvalidFormat("Region data too short, need at least 8192 bytes for header".to_string()));
}
let mut region = Xbox360Region {
entries: Default::default(),
raw_data: data.to_vec(),
};
for z in 0..REGION_SIZE_Z {
for x in 0..REGION_SIZE_X {
let index = z * REGION_SIZE_X + x;
let offset_raw = read_be_u32(&data[index * 4..index * 4 + 4]);
let timestamp_raw = read_be_u32(&data[4096 + index * 4..4096 + index * 4 + 4]);
let offset = offset_raw >> 8;
let sector_count = offset_raw & 0xFF;
region.entries[x][z] = RegionEntry {
offset,
sector_count,
timestamp: timestamp_raw,
exists: offset_raw != 0 && sector_count != 0,
};
}
}
Ok(region)
}
pub fn read_region_chunk(region: &Xbox360Region, x: usize, z: usize) -> Result<Vec<u8>, ConversionError> {
if x >= REGION_SIZE_X || z >= REGION_SIZE_Z {
return Err(ConversionError::InvalidFormat(format!(
"Chunk coordinates out of range: ({}, {}), max ({}, {})",
x, z, REGION_SIZE_X, REGION_SIZE_Z
)));
}
let entry = &region.entries[x][z];
if !entry.exists {
return Err(ConversionError::MissingData(format!(
"Chunk ({}, {}) does not exist in region", x, z
)));
}
let sector_offset = (entry.offset as usize) * SECTOR_SIZE as usize;
let total_size = (entry.sector_count as usize) * SECTOR_SIZE as usize;
if sector_offset + total_size > region.raw_data.len() {
return Err(ConversionError::InvalidFormat(format!(
"Chunk data at sector offset {} with size {} exceeds region data length {}",
sector_offset, total_size, region.raw_data.len()
)));
}
let sector_data = &region.raw_data[sector_offset..sector_offset + 5];
let _compressed_size = read_be_u32(&sector_data[0..4]);
let _compression_scheme = sector_data[4];
let chunk_data = region.raw_data[sector_offset..sector_offset + total_size].to_vec();
Ok(chunk_data)
}
pub fn read_java_mcr_region(data: &[u8]) -> Result<Vec<(usize, usize, Vec<u8>)>, ConversionError> {
if data.len() < 8192 {
return Err(ConversionError::InvalidFormat("MCR data too short".to_string()));
}
let mut chunks = Vec::new();
for z in 0..REGION_SIZE_Z {
for x in 0..REGION_SIZE_X {
let index = z * REGION_SIZE_X + x;
let offset_raw = read_be_u32(&data[index * 4..index * 4 + 4]);
if offset_raw == 0 {
continue;
}
let sector_offset = (offset_raw >> 8) as usize;
let _sector_count = (offset_raw & 0xFF) as usize;
let file_offset = sector_offset * SECTOR_SIZE as usize;
if file_offset + 5 > data.len() {
continue;
}
let chunk_len = read_be_u32(&data[file_offset..file_offset + 4]) as usize;
let compression = data[file_offset + 4];
if chunk_len == 0 || file_offset + 5 + chunk_len > data.len() {
continue;
}
let mut chunk_data = vec![0u8; 5 + chunk_len];
chunk_data[0..4].copy_from_slice(&data[file_offset..file_offset + 4]);
chunk_data[4] = compression;
chunk_data[5..].copy_from_slice(&data[file_offset + 5..file_offset + 5 + chunk_len]);
chunks.push((x, z, chunk_data));
}
}
Ok(chunks)
}
pub fn read_java_mca_region(data: &[u8]) -> Result<Vec<(usize, usize, Vec<u8>)>, ConversionError> {
read_java_mcr_region(data)
}
fn read_be_u32(bytes: &[u8]) -> u32 {
if bytes.len() < 4 { return 0; }
((bytes[0] as u32) << 24) | ((bytes[1] as u32) << 16) | ((bytes[2] as u32) << 8) | (bytes[3] as u32)
}

View file

@ -1,54 +0,0 @@
use std::collections::HashMap;
use byteorder::{LittleEndian, WriteBytesExt};
use crate::console2lce::error::ConversionError;
const SAVE_MAGIC: [u8; 6] = [0x01, 0x00, 0x00, 0x00, 0x01, 0x00];
const CONTAINER_VERSION: u32 = 2;
pub fn write_save_data_container(
files: &[(String, Vec<u8>)],
) -> Result<Vec<u8>, ConversionError> {
let mut data = Vec::new();
data.extend_from_slice(&SAVE_MAGIC);
data.write_u32::<LittleEndian>(CONTAINER_VERSION)?;
data.write_u32::<LittleEndian>(files.len() as u32)?;
let mut file_offsets = Vec::new();
for (name, content) in files {
file_offsets.push((name.clone(), data.len() as u64, content.len() as u64));
data.extend_from_slice(content);
while data.len() % 4 != 0 {
data.push(0);
}
}
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
for (name, offset, length) in &file_offsets {
data.write_u64::<LittleEndian>(*offset)?;
data.write_u64::<LittleEndian>(*length)?;
data.write_u64::<LittleEndian>(timestamp)?;
let name_bytes = name.as_bytes();
data.write_u16::<LittleEndian>(name_bytes.len() as u16)?;
data.extend_from_slice(name_bytes);
data.push(0);
while data.len() % 4 != 0 {
data.push(0);
}
}
Ok(data)
}
pub fn write_save_data_ms(
level_dat: &[u8],
regions: &HashMap<String, Vec<u8>>,
) -> Result<Vec<u8>, ConversionError> {
let mut files = Vec::new();
files.push(("level.dat".to_string(), level_dat.to_vec()));
for (path, region_data) in regions {
files.push((path.clone(), region_data.clone()));
}
write_save_data_container(&files)
}

View file

@ -1,635 +0,0 @@
use crate::console2lce::error::ConversionError;
use lzxd::{Lzxd, WindowSize};
const CON_MAGIC: u32 = 0x434F4E20;
fn read_be_u32(bytes: &[u8]) -> u32 {
((bytes[0] as u32) << 24) | ((bytes[1] as u32) << 16) | ((bytes[2] as u32) << 8) | (bytes[3] as u32)
}
fn read_be_u16(bytes: &[u8]) -> u16 {
((bytes[0] as u16) << 8) | (bytes[1] as u16)
}
fn read_le_u16(bytes: &[u8]) -> u16 {
(bytes[0] as u16) | ((bytes[1] as u16) << 8)
}
fn read_le_u24(bytes: &[u8]) -> i32 {
(bytes[0] as i32) | ((bytes[1] as i32) << 8) | ((bytes[2] as i32) << 16)
}
pub fn is_stfs_package(data: &[u8]) -> bool {
if data.len() < 4 { return false; }
read_be_u32(&data[0..4]) == CON_MAGIC
}
struct StfsVD {
_size: u8,
block_separation: u8,
file_table_block_count: u16,
file_table_block_num: i32,
alloc_block_count: u32,
_unallocated_block_count: u32,
}
fn read_stfs_vd(data: &[u8]) -> Result<StfsVD, ConversionError> {
if data.len() < 0x24 {
return Err(ConversionError::InvalidFormat("STFS VD too short".to_string()));
}
let size = data[0];
let block_separation = data[2];
let file_table_block_count = read_le_u16(&data[3..5]);
let file_table_block_num = read_le_u24(&data[5..8]);
let alloc_block_count = read_be_u32(&data[0x1C..0x20]);
let unallocated_block_count = read_be_u32(&data[0x20..0x24]);
Ok(StfsVD {
_size: size,
block_separation,
file_table_block_count,
file_table_block_num,
alloc_block_count,
_unallocated_block_count: unallocated_block_count,
})
}
struct StfsPackage<'a> {
data: &'a [u8],
header_size: u32,
vd: StfsVD,
package_sex: u8, //neo: 0=female, 1=male
block_step: [u32; 2],
first_hash_addr: u32,
top_level: u8,
top_table: Vec<HashEntry>,
alloc_block_count: u32,
}
#[derive(Clone, Copy)]
struct HashEntry {
_hash: [u8; 20],
status: u8,
next_block: i32,
}
impl<'a> StfsPackage<'a> {
fn parse(data: &'a [u8]) -> Result<Self, ConversionError> {
if data.len() < 0x400 {
return Err(ConversionError::InvalidFormat("STFS data too short for header".to_string()));
}
if !is_stfs_package(data) {
return Err(ConversionError::InvalidFormat("Not a valid STFS package".to_string()));
}
let header_size = read_be_u32(&data[0x340..0x344]);
let content_type = read_be_u32(&data[0x344..0x348]);
if content_type != 1 {
return Err(ConversionError::InvalidFormat(format!(
"Not a savegame (content_type={})", content_type
)));
}
let file_system = read_be_u32(&data[0x3A9..0x3AD]);
if file_system != 0 {
return Err(ConversionError::InvalidFormat("Not STFS format".to_string()));
}
let vd = read_stfs_vd(&data[0x379..])?;
let alloc_block_count = vd.alloc_block_count;
if alloc_block_count == 0 {
return Err(ConversionError::InvalidFormat("No allocated blocks".to_string()));
}
let package_sex = (!vd.block_separation) & 1;
let block_step: [u32; 2] = if package_sex == 0 {
[0xAB, 0x718F]
} else {
[0xAC, 0x723A]
};
let first_hash_addr = (header_size + 0x0FFF) & !0x0FFF;
let top_level = if alloc_block_count <= 0xAA {
0
} else if alloc_block_count <= 0x70E4 {
1
} else if alloc_block_count <= 0x4AF768 {
2
} else {
return Err(ConversionError::InvalidFormat("Too many allocated blocks".to_string()));
};
let top_true_block = compute_level_n_backing_hash_block_number(0, top_level, &block_step, package_sex);
let base_addr = (top_true_block << 12) + first_hash_addr;
let top_addr = base_addr + (((vd.block_separation & 2) as u32) << 11);
let top_addr = top_addr as usize;
let data_blocks_per_level: [u32; 3] = [1, 0xAA, 0x70E4];
let divisor = data_blocks_per_level[top_level as usize];
let mut entry_count = alloc_block_count / divisor;
if alloc_block_count > 0x70E4 && alloc_block_count % 0x70E4 != 0 {
entry_count += 1;
} else if alloc_block_count > 0xAA && alloc_block_count % 0xAA != 0 {
entry_count += 1;
}
let mut top_table = Vec::with_capacity(entry_count as usize);
for i in 0..entry_count {
let off = top_addr + (i as usize) * 24;
if off + 24 > data.len() {
break;
}
let mut hash = [0u8; 20];
hash.copy_from_slice(&data[off..off + 20]);
let status = data[off + 20];
let next_block = read_le_u24(&data[off + 21..off + 24]);
top_table.push(HashEntry {
_hash: hash,
status,
next_block,
});
}
Ok(StfsPackage {
data,
header_size,
vd,
package_sex,
block_step,
first_hash_addr,
top_level,
top_table,
alloc_block_count,
})
}
fn block_to_address(&self, block_num: u32) -> u32 {
(self.compute_backing_data_block_number(block_num) << 12) + self.first_hash_addr
}
fn compute_backing_data_block_number(&self, block_num: u32) -> u32 {
let psex = self.package_sex as u32;
let to_return = (((block_num + 0xAA) / 0xAA) << psex) + block_num;
if block_num < 0xAA {
return to_return;
} else if block_num < 0x70E4 {
return to_return + (((block_num + 0x70E4) / 0x70E4) << psex);
} else {
return (1 << psex) + (to_return + (((block_num + 0x70E4) / 0x70E4) << psex));
}
}
fn get_hash_entry(&self, block_num: u32) -> Result<HashEntry, ConversionError> {
if block_num >= self.alloc_block_count {
return Err(ConversionError::InvalidFormat("Block number out of range".to_string()));
}
let hash_addr = self.get_hash_address_of_block(block_num);
let off = hash_addr as usize;
if off + 24 > self.data.len() {
return Err(ConversionError::InvalidFormat("Hash address out of file bounds".to_string()));
}
let mut hash = [0u8; 20];
hash.copy_from_slice(&self.data[off..off + 20]);
let status = self.data[off + 20];
let next_block = read_le_u24(&self.data[off + 21..off + 24]);
Ok(HashEntry { _hash: hash, status, next_block })
}
fn get_hash_address_of_block(&self, block_num: u32) -> u32 {
let level0 = compute_level_0_backing_hash_block_number(block_num, &self.block_step, self.package_sex);
let mut hash_addr = (level0 << 12) + self.first_hash_addr;
hash_addr += (block_num % 0xAA) * 24;
match self.top_level {
0 => {
hash_addr += (self.vd.block_separation as u32 & 2) << 11;
}
1 => {
let idx = (block_num / 0xAA) as usize;
if idx < self.top_table.len() {
hash_addr += ((self.top_table[idx].status & 0x40) as u32) << 6;
}
}
2 => {
let l1_idx = (block_num / 0x70E4) as usize;
if l1_idx < self.top_table.len() {
let l1_off = (self.top_table[l1_idx].status as u32 & 0x40) << 6;
let l1_block = compute_level_1_backing_hash_block_number(block_num, &self.block_step, self.package_sex);
let pos = ((l1_block << 12) + self.first_hash_addr + l1_off) +
((block_num % 0xAA) * 24);
let pos = pos as usize;
if pos + 21 <= self.data.len() {
hash_addr += (self.data[pos + 20] as u32 & 0x40) << 6;
}
}
}
_ => {}
}
hash_addr
}
fn read_file_listing(&self) -> Result<Vec<StfsFileEntry>, ConversionError> {
let mut entries = Vec::new();
let block_count = self.vd.file_table_block_count as u32;
let mut block = self.vd.file_table_block_num as u32;
for _ in 0..block_count {
let addr = self.block_to_address(block) as usize;
if addr + 0x1000 > self.data.len() {
break;
}
for i in 0..0x40u32 {
let off = addr + (i as usize) * 0x40;
if off + 0x40 > self.data.len() {
break;
}
let name_bytes = &self.data[off..off + 0x28];
let name_len_byte = self.data[off + 0x28];
let name_len = name_len_byte & 0x3F;
if name_len == 0 {
continue;
}
let name = String::from_utf8_lossy(&name_bytes[..name_len as usize]).to_string();
if name.is_empty() {
break;
}
let blocks_for_file = read_le_u24(&self.data[off + 0x29..off + 0x2C]);
let starting_block_num = read_le_u24(&self.data[off + 0x2F..off + 0x32]);
let path_indicator = read_be_u16(&self.data[off + 0x32..off + 0x34]);
let file_size = read_be_u32(&self.data[off + 0x34..off + 0x38]);
let created_ts = read_be_u32(&self.data[off + 0x38..off + 0x3C]);
let accessed_ts = read_be_u32(&self.data[off + 0x3C..off + 0x40]);
let flags = name_len_byte >> 6;
entries.push(StfsFileEntry {
name,
flags,
blocks_for_file,
starting_block_num,
path_indicator,
file_size,
created_ts,
accessed_ts,
});
}
let he = self.get_hash_entry(block)?;
block = he.next_block as u32;
}
Ok(entries)
}
fn extract_file(&self, entry: &StfsFileEntry) -> Result<Vec<u8>, ConversionError> {
let file_size = entry.file_size;
if file_size == 0 {
return Ok(Vec::new());
}
if entry.flags & 1 != 0 {
let mut out = Vec::with_capacity(file_size as usize);
let start_addr = self.block_to_address(entry.starting_block_num as u32);
let pos = start_addr as usize;
let block_count = (compute_level_0_backing_hash_block_number(
entry.starting_block_num as u32, &self.block_step, self.package_sex
) + self.block_step[0]) - ((start_addr - self.first_hash_addr) >> 12);
let initial = if (entry.blocks_for_file as u32) <= block_count {
let sz = entry.file_size as usize;
if pos + sz <= self.data.len() {
out.extend_from_slice(&self.data[pos..pos + sz]);
}
return Ok(out);
} else {
let amount = (block_count << 12) as usize;
if pos + amount <= self.data.len() {
out.extend_from_slice(&self.data[pos..pos + amount]);
}
amount
};
let mut remaining = (file_size as usize).wrapping_sub(initial);
let mut cur_pos = pos + initial;
while remaining >= 0xAA000 {
cur_pos += self.get_hash_table_skip_size(cur_pos as u32) as usize;
let end = (cur_pos + 0xAA000).min(self.data.len());
out.extend_from_slice(&self.data[cur_pos..end]);
remaining = remaining.wrapping_sub(0xAA000);
cur_pos = end;
}
if remaining > 0 {
cur_pos += self.get_hash_table_skip_size(cur_pos as u32) as usize;
let end = (cur_pos + remaining).min(self.data.len());
out.extend_from_slice(&self.data[cur_pos..end]);
}
Ok(out)
} else {
let full_reads = file_size / 0x1000;
let remainder = file_size % 0x1000;
let mut out = Vec::with_capacity(file_size as usize);
let mut block = entry.starting_block_num as u32;
for _ in 0..full_reads {
let addr = self.block_to_address(block) as usize;
if addr + 0x1000 <= self.data.len() {
out.extend_from_slice(&self.data[addr..addr + 0x1000]);
}
block = self.get_hash_entry(block)?.next_block as u32;
}
if remainder > 0 {
let addr = self.block_to_address(block) as usize;
if addr + remainder as usize <= self.data.len() {
out.extend_from_slice(&self.data[addr..addr + remainder as usize]);
}
}
Ok(out)
}
}
fn get_hash_table_skip_size(&self, table_addr: u32) -> u32 {
let psex = self.package_sex as u32;
let true_block = (table_addr - self.first_hash_addr) >> 12;
if true_block == 0 {
return 0x1000 << psex;
}
let mut tb = true_block;
if tb == self.block_step[1] {
return 0x3000 << psex;
} else if tb > self.block_step[1] {
tb -= self.block_step[1] + (1 << psex);
}
if tb == self.block_step[0] || tb % self.block_step[1] == 0 {
return 0x2000 << psex;
}
0x1000 << psex
}
}
#[derive(Debug, Clone)]
struct StfsFileEntry {
name: String,
flags: u8,
blocks_for_file: i32,
starting_block_num: i32,
path_indicator: u16,
file_size: u32,
created_ts: u32,
accessed_ts: u32,
}
fn compute_level_n_backing_hash_block_number(
block_num: u32, level: u8, block_step: &[u32; 2], package_sex: u8,
) -> u32 {
match level {
0 => compute_level_0_backing_hash_block_number(block_num, block_step, package_sex),
1 => compute_level_1_backing_hash_block_number(block_num, block_step, package_sex),
2 => block_step[1],
_ => 0,
}
}
fn compute_level_0_backing_hash_block_number(
block_num: u32, block_step: &[u32; 2], package_sex: u8,
) -> u32 {
let psex = package_sex as u32;
if block_num < 0xAA { return 0; }
let mut num = (block_num / 0xAA) * block_step[0];
num += ((block_num / 0x70E4) + 1) << psex;
if block_num / 0x70E4 == 0 { return num; }
num + (1 << psex)
}
fn compute_level_1_backing_hash_block_number(
block_num: u32, block_step: &[u32; 2], package_sex: u8,
) -> u32 {
let psex = package_sex as u32;
if block_num < 0x70E4 { return block_step[0]; }
(1 << psex) + (block_num / 0x70E4) * block_step[1]
}
pub fn extract_savegame_from_stfs(data: &[u8]) -> Result<Vec<u8>, ConversionError> {
let pkg = StfsPackage::parse(data)?;
let listing = pkg.read_file_listing()?;
let entry = listing.iter().find(|e| e.name == "savegame.dat")
.ok_or_else(|| ConversionError::InvalidFormat("savegame.dat not found in STFS listing".to_string()))?;
eprintln!("[stfs] found savegame.dat: size={} blocks={} start_block={}",
entry.file_size, entry.blocks_for_file, entry.starting_block_num);
let raw_data = pkg.extract_file(entry)?;
eprintln!("[stfs] extracted {} bytes of savegame.dat", raw_data.len());
Ok(raw_data)
}
pub fn try_decompress_stfs_savegame(data: &[u8]) -> Result<Vec<u8>, ConversionError> {
if data.len() < 12 {
return Err(ConversionError::InvalidFormat("STFS savegame data too short for header".to_string()));
}
let total_size = read_be_u32(&data[0..4]);
let decompressed_size = u64::from(read_be_u32(&data[4..8])) << 32 | read_be_u32(&data[8..12]) as u64;
let src_size = total_size.wrapping_sub(8) as usize;
let decompressed_size = decompressed_size as usize;
eprintln!("[stfs] savegame header: total_size={} src_size={} decompressed_size={}",
total_size, src_size, decompressed_size);
if src_size == 0 || src_size > data.len().saturating_sub(12) {
return Err(ConversionError::InvalidFormat(format!(
"Invalid compressed size: {} (data len: {})", src_size, data.len()
)));
}
if decompressed_size == 0 || decompressed_size > 100_000_000 {
return Err(ConversionError::InvalidFormat(format!(
"Invalid decompressed size: {}", decompressed_size
)));
}
let compressed = &data[12..12 + src_size];
decompress_lzx_chunked(compressed, decompressed_size)
}
pub fn decompress_lzx_chunked(data: &[u8], decompressed_size: usize) -> Result<Vec<u8>, ConversionError> {
let mut lzxd = Lzxd::new(WindowSize::KB64);
let mut output = Vec::with_capacity(decompressed_size);
let mut pos = 0;
let mut num_chunks = 0;
let mut comp_count = 0;
let mut uncomp_count = 0;
let mut lzx_fallbacks = 0;
while pos < data.len() && output.len() < decompressed_size {
if pos + 2 > data.len() {
break;
}
let chunk_size = read_be_u16(&data[pos..pos + 2]) as usize;
pos += 2;
if chunk_size == 0 {
break;
}
let is_compressed = (chunk_size & 0x8000) != 0;
let actual_size = chunk_size & 0x7FFF;
num_chunks += 1;
if pos + actual_size > data.len() {
eprintln!("[lzx] chunk {} header overrun: need {} have {} at pos {}",
num_chunks, actual_size, data.len() - pos + 2, pos - 2);
break;
}
let chunk_data = &data[pos..pos + actual_size];
pos += actual_size;
if is_compressed {
let out_len = if output.len() + 32768 > decompressed_size {
decompressed_size - output.len()
} else {
32768
};
comp_count += 1;
match lzxd.decompress_next(chunk_data, out_len) {
Ok(decompressed) => {
eprintln!("[lzx] chunk {} C: compressed={} decompressed={} total_so_far={}",
num_chunks, actual_size, decompressed.len(), output.len() + decompressed.len());
output.extend_from_slice(decompressed);
}
Err(e) => {
return Err(ConversionError::DecompressionFailed(
format!("chunk {} LZXD failed at pos {}: {}", num_chunks, pos - actual_size - 2, e)
));
}
}
} else {
let mut chunk_lzxd = Lzxd::new(WindowSize::KB32);
match chunk_lzxd.decompress_next(chunk_data, 32768) {
Ok(decompressed) => {
lzx_fallbacks += 1;
eprintln!("[lzx] chunk {} U->C (LZX fallback): {} -> {} total_so_far={}",
num_chunks, actual_size, decompressed.len(), output.len() + decompressed.len());
output.extend_from_slice(decompressed);
}
Err(_) => {
eprintln!("[lzx] chunk {} U: size={} total_so_far={}",
num_chunks, actual_size, output.len() + actual_size);
output.extend_from_slice(chunk_data);
uncomp_count += 1;
}
}
}
}
eprintln!("[lzx] processed {} chunks ({} marked C + {} LZX-fallback + {} pure U), produced {} of {} expected bytes, consumed {}/{}",
num_chunks, comp_count, lzx_fallbacks, uncomp_count, output.len(), decompressed_size, pos, data.len());
output.truncate(decompressed_size);
Ok(output)
}
pub fn try_decompress_savegame(data: &[u8]) -> Result<Vec<u8>, ConversionError> {
if data.len() < 8 {
return Err(ConversionError::InvalidFormat("Savegame data too short".to_string()));
}
let magic = &data[0..8];
let expected_magic = [0x0B, 0xFC, 0x4A, 0x46, 0xAE, 0x7A, 0x43, 0x2B];
if magic != expected_magic {
return Err(ConversionError::InvalidFormat("Invalid savegame magic".to_string()));
}
if data.len() < 16 {
return Err(ConversionError::InvalidFormat("Savegame data too short for header".to_string()));
}
let decompressed_size = read_be_u32(&data[8..12]) as usize;
let compression_type = read_be_u32(&data[12..16]);
let payload = &data[16..];
match compression_type {
0 => {
if payload.len() >= decompressed_size {
Ok(payload[..decompressed_size].to_vec())
} else {
Ok(payload.to_vec())
}
}
1 => {
decompress_lzx(payload, decompressed_size)
}
2 => {
use std::io::Read;
let mut decoder = flate2::read::ZlibDecoder::new(payload);
let mut out = Vec::with_capacity(decompressed_size);
decoder.read_to_end(&mut out)?;
Ok(out)
}
3 => {
decode_rle(payload, decompressed_size)
}
_ => Err(ConversionError::DecompressionFailed(
format!("Unknown compression type: {}", compression_type)
)),
}
}
fn decompress_lzx(data: &[u8], decompressed_size: usize) -> Result<Vec<u8>, ConversionError> {
let mut output = Vec::with_capacity(decompressed_size);
let mut pos = 0;
while pos < data.len() && output.len() < decompressed_size {
if pos + 2 > data.len() {
break;
}
let chunk_size = read_be_u16(&data[pos..pos + 2]) as usize;
pos += 2;
if chunk_size == 0 {
break;
}
let is_compressed = (chunk_size & 0x8000) != 0;
let actual_size = chunk_size & 0x7FFF;
if is_compressed {
if pos + actual_size > data.len() {
break;
}
let compressed_chunk = &data[pos..pos + actual_size];
pos += actual_size;
let mut lzxd = Lzxd::new(WindowSize::KB32);
match lzxd.decompress_next(compressed_chunk, 32768) {
Ok(decompressed) => {
output.extend_from_slice(decompressed);
}
Err(e) => {
eprintln!("[lzx] chunk LZXD decompress failed at pos {}: {}; filling with zeros",
pos - actual_size - 2, e);
output.extend_from_slice(&vec![0u8; 32768]);
}
}
} else {
if pos + actual_size > data.len() {
break;
}
output.extend_from_slice(&data[pos..pos + actual_size]);
pos += actual_size;
}
}
output.truncate(decompressed_size);
Ok(output)
}
pub fn decode_rle(data: &[u8], decompressed_size: usize) -> Result<Vec<u8>, ConversionError> {
let mut output = Vec::with_capacity(decompressed_size);
let mut pos = 0;
while pos < data.len() && output.len() < decompressed_size {
let b = data[pos];
pos += 1;
if b != 0xFF {
if output.len() + 1 > decompressed_size {
break;
}
output.push(b);
} else {
if pos + 1 >= data.len() {
break;
}
let run_len = data[pos] as usize;
let value = data[pos + 1];
pos += 2;
let actual_len = std::cmp::min(run_len, decompressed_size - output.len());
for _ in 0..actual_len {
output.push(value);
}
}
}
output.truncate(decompressed_size);
Ok(output)
}

View file

@ -1,26 +0,0 @@
#[derive(Debug, Clone, Copy)]
pub struct WorldProfile {
pub name: &'static str,
pub xz_size: u32,
pub hell_scale: u32,
pub flat: bool,
}
pub const PROFILES: &[WorldProfile] = &[
WorldProfile { name: "classic", xz_size: 54, hell_scale: 54, flat: false },
WorldProfile { name: "small", xz_size: 64, hell_scale: 64, flat: false },
WorldProfile { name: "medium", xz_size: 192, hell_scale: 192, flat: false },
WorldProfile { name: "large", xz_size: 320, hell_scale: 320, flat: false },
WorldProfile { name: "flat", xz_size: 54, hell_scale: 54, flat: true },
WorldProfile { name: "flat-small", xz_size: 64, hell_scale: 64, flat: true },
WorldProfile { name: "flat-medium", xz_size: 192, hell_scale: 192, flat: true },
WorldProfile { name: "flat-large", xz_size: 320, hell_scale: 320, flat: true },
];
pub fn get_profile(name: &str) -> Option<&'static WorldProfile> {
PROFILES.iter().find(|p| p.name == name)
}
pub fn world_center_offset(xz_size: u32) -> i32 {
(xz_size as i32) * 8
}

View file

@ -4,7 +4,6 @@ mod config;
mod util;
mod platform;
mod networking;
pub mod console2lce;
mod commands;
use std::collections::HashMap;
use std::sync::Arc;

View file

@ -2,8 +2,6 @@ import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { TauriService } from "../../services/TauriService";
type WorldType = "java" | "xbox360" | "windows64" | null;
export default function ImportWorldModal({
isOpen,
onClose,
@ -19,34 +17,12 @@ export default function ImportWorldModal({
targetInstanceId: string;
targetInstanceName: string;
}) {
const [worldType, setWorldType] = useState<WorldType>(null);
const [focusIndex, setFocusIndex] = useState(0);
const [status, setStatus] = useState("");
const [error, setError] = useState("");
const [isImporting, setIsImporting] = useState(false);
const typeOptions: { id: WorldType; label: string; desc: string }[] = [
{
id: "java",
label: "Java Edition",
desc: "Import a Java world folder (contains level.dat)",
},
{
id: "xbox360",
label: "Xbox 360",
desc: "Import an Xbox 360 STFS save (.bin / .dat)",
},
{
id: "windows64",
label: "Windows64",
desc: "Copy an existing LCE save (.ms file or GameHDD folder)",
},
];
useEffect(() => {
if (!isOpen) {
setWorldType(null);
setFocusIndex(0);
setStatus("");
setError("");
setIsImporting(false);
@ -54,64 +30,28 @@ export default function ImportWorldModal({
}, [isOpen]);
const handleImport = async () => {
if (!worldType || !targetInstanceId) return;
if (!targetInstanceId) return;
playPressSound();
setIsImporting(true);
setError("");
setStatus("Selecting source...");
try {
let inputPath = "";
let worldName = "";
if (worldType === "java") {
setStatus("Selecting Java world folder...");
const folder = await TauriService.pickFolder();
if (!folder) {
setIsImporting(false);
return;
}
inputPath = folder;
worldName = deriveWorldName(folder);
setStatus("Converting Java world to LCE...");
} else if (worldType === "xbox360") {
setStatus("Selecting Xbox 360 save...");
const file = await TauriService.pickFile("Select Xbox 360 save", [
"*.bin",
"*.dat",
"*",
]);
if (!file) {
setIsImporting(false);
return;
}
inputPath = file;
worldName = deriveWorldName(file);
setStatus("Converting Xbox 360 save to LCE...");
} else {
setStatus("Selecting LCE save folder or .ms file...");
const picked = await TauriService.pickFile(
"Select saveData.ms or GameHDD folder",
["*.ms", "*"],
);
if (!picked) {
setIsImporting(false);
return;
}
inputPath = picked;
worldName = picked.endsWith(".ms")
? deriveWorldName(picked)
: deriveWorldName(picked);
setStatus("Copying LCE save...");
setStatus("Selecting LCE save folder or .ms file...");
const picked = await TauriService.pickFile(
"Select saveData.ms or GameHDD folder",
["*.ms", "*"],
);
if (!picked) {
setIsImporting(false);
return;
}
const worldName = deriveWorldName(picked);
setStatus("Copying LCE save...");
const instancePath = await TauriService.getInstancePath(targetInstanceId);
const saveDir = `${instancePath}/Windows64/GameHDD/${worldName}`;
if (worldType === "windows64") {
await TauriService.importWorld(inputPath, `${saveDir}/saveData.ms`);
} else {
await TauriService.importWorld(inputPath, `${saveDir}/saveData.ms`);
}
await TauriService.importWorld(picked, `${saveDir}/saveData.ms`);
setStatus(`World imported into "${targetInstanceName}"!`);
setTimeout(() => {
@ -129,25 +69,6 @@ export default function ImportWorldModal({
playBackSound();
if (isImporting) return;
onClose();
} else if (
e.key === "ArrowDown" ||
e.key === "ArrowUp" ||
e.key === "Tab"
) {
e.preventDefault();
const max = typeOptions.length + 1;
setFocusIndex((prev) => {
if (e.key === "ArrowUp") return (prev - 1 + max) % max;
return (prev + 1) % max;
});
} else if (e.key === "Enter") {
if (focusIndex < typeOptions.length) {
const opt = typeOptions[focusIndex];
setWorldType(opt.id);
playPressSound();
} else {
onClose();
}
}
};
@ -155,74 +76,16 @@ export default function ImportWorldModal({
if (!isOpen) return;
window.addEventListener("keydown", handleKey);
return () => window.removeEventListener("keydown", handleKey);
}, [isOpen, focusIndex, worldType, isImporting]);
}, [isOpen, isImporting]);
if (!isOpen) return null;
if (worldType && !isImporting) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 w-screen h-screen z-[100] flex items-center justify-center bg-black/80 backdrop-blur-md"
>
<div
className="relative w-[420px] p-6 flex flex-col items-center shadow-2xl"
style={{
backgroundImage: "url('/images/frame_background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-2 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
Import {typeOptions.find((t) => t.id === worldType)?.label}
</h2>
<p className="text-white text-sm mc-text-shadow mb-4 text-center">
Destination:{" "}
<span className="text-[#FFFF55]">{targetInstanceName}</span>
</p>
<p className="text-gray-400 text-xs mc-text-shadow mb-4 text-center">
{typeOptions.find((t) => t.id === worldType)?.desc}
</p>
<div className="flex gap-4 w-full justify-center">
<button
onClick={() => {
setWorldType(null);
playBackSound();
}}
className="w-32 h-10 flex items-center justify-center text-xl text-white mc-text-shadow"
style={{
backgroundImage: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Back
</button>
<button
onClick={handleImport}
className="w-40 h-10 flex items-center justify-center text-xl text-white mc-text-shadow hover:text-[#FFFF55]"
style={{
backgroundImage: "url('/images/button_highlighted.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Select File
</button>
</div>
</div>
</motion.div>
);
}
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 w-screen h-screen z-[100] flex items-center justify-center bg-black/80 backdrop-blur-md outline-none border-none"
className="fixed inset-0 w-screen h-screen z-[100] flex items-center justify-center bg-black/80 backdrop-blur-md"
>
<div
className="relative w-[450px] p-6 flex flex-col items-center shadow-2xl"
@ -242,50 +105,9 @@ export default function ImportWorldModal({
<span className="text-[#FFFF55]">{targetInstanceName}</span>
</p>
<p className="text-gray-400 text-xs mc-text-shadow mb-4 text-center">
What type of world are you importing?
Select an existing LCE save (.ms file or GameHDD folder)
</p>
<div className="w-full mb-4 flex flex-col gap-2">
{typeOptions.map((opt, i) => {
const isSelected = worldType === opt.id;
const isFocused = focusIndex === i;
return (
<div
key={opt.id}
onClick={() => {
playPressSound();
setWorldType(opt.id);
}}
onMouseEnter={() => setFocusIndex(i)}
className={`w-full px-4 py-3 cursor-pointer flex flex-col gap-0.5 transition-all outline-none border-none ${
isSelected
? "bg-white/15 border-l-4 border-[#FFFF55]"
: isFocused
? "bg-white/10 border-l-4 border-[#FFFF55]"
: "bg-black/20 hover:bg-black/30 border-l-4 border-transparent"
}`}
style={{ imageRendering: "pixelated" }}
>
<div className="flex items-center gap-3">
<div
className={`w-4 h-4 rounded-full border-2 flex items-center justify-center shrink-0 ${
isSelected ? "border-[#FFFF55]" : "border-gray-500"
}`}
>
{isSelected && (
<div className="w-2 h-2 rounded-full bg-[#FFFF55]" />
)}
</div>
<span className="text-white text-base font-bold mc-text-shadow">
{opt.label}
</span>
</div>
<p className="text-gray-400 text-xs ml-7">{opt.desc}</p>
</div>
);
})}
</div>
{error && (
<div className="text-red-500 text-center mc-text-shadow uppercase text-xs tracking-widest mb-3">
{error}
@ -294,27 +116,30 @@ export default function ImportWorldModal({
<div className="flex gap-4 w-full justify-center">
<button
onMouseEnter={() => setFocusIndex(typeOptions.length)}
onClick={() => {
playBackSound();
onClose();
}}
className={`w-32 h-10 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none ${
focusIndex === typeOptions.length
? "text-[#FFFF55]"
: "text-white"
}`}
className="w-32 h-10 flex items-center justify-center text-xl text-white mc-text-shadow"
style={{
backgroundImage:
focusIndex === typeOptions.length
? "url('/images/button_highlighted.png')"
: "url('/images/Button_Background.png')",
backgroundImage: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Cancel
</button>
<button
onClick={handleImport}
className="w-40 h-10 flex items-center justify-center text-xl text-white mc-text-shadow hover:text-[#FFFF55]"
style={{
backgroundImage: "url('/images/button_highlighted.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Select File
</button>
</div>
</>
) : (

View file

@ -48,7 +48,7 @@ class RPC {
activity.setDetails(details);
activity.setState(state);
activity.setActivity(ActivityType.Playing);
activity.setParty(new Party(`emerald_${username}`, [1, 2]));
if (!state.startsWith("Logged")) {activity.setParty(new Party(`emerald_${username}`, [1, 2]));}
const assets = new Assets();
assets.setLargeImage("logo");
assets.setLargeText("LCE Emerald Launcher");

View file

@ -294,14 +294,7 @@ export class TauriService {
static async importWorld(
inputPath: string,
outputPath: string,
profile?: string,
preserveEntities?: boolean,
): Promise<string> {
return invoke("import_world", {
inputPath,
outputPath,
profile: profile ?? null,
preserveEntities: preserveEntities ?? null,
});
return invoke("import_world", { inputPath, outputPath });
}
}