mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-07-18 00:07:09 +00:00
chore: remove console2lce because IT DOESNT WORK
This commit is contained in:
parent
f85829c88c
commit
3a7335f43d
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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, ®ions)
|
||||
.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(®ion_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, ®ions) {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 §ions {
|
||||
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(§ion.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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = ®ion.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 = ®ion.raw_data[sector_offset..sector_offset + 5];
|
||||
let _compressed_size = read_be_u32(§or_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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue