diff --git a/src-tauri/src/commands/java2lce/block_mapping.rs b/src-tauri/src/commands/java2lce/block_mapping.rs new file mode 100644 index 0000000..cee48fc --- /dev/null +++ b/src-tauri/src/commands/java2lce/block_mapping.rs @@ -0,0 +1,1353 @@ +use std::collections::HashMap; +use super::nbt::NbtCompound; +#[derive(Clone, Copy, Default)] +pub struct LegacyBlockState { + pub id: u8, + pub data: u8, +} + +pub fn map_modern_block_state(compound: &NbtCompound) -> LegacyBlockState { + map_modern_block_state_inner(compound, None) +} + +pub fn map_modern_block_state_with_context( + compound: &NbtCompound, + unknown_blocks: Option<&mut Vec>, +) -> LegacyBlockState { + map_modern_block_state_inner(compound, unknown_blocks) +} + +fn map_modern_block_state_inner( + compound: &NbtCompound, + mut unknown_blocks: Option<&mut Vec>, +) -> LegacyBlockState { + let name_raw = compound.string("Name").unwrap_or("minecraft:air"); + let name = name_raw.strip_prefix("minecraft:").unwrap_or(name_raw); + let properties = compound.compound("Properties"); + if let Some(fluid) = try_map_fluid(name, properties) { + return fluid; + } + if let Some(slab) = try_map_slab(name, properties) { + return slab; + } + if let Some(directional) = try_map_directional(name, properties) { + return directional; + } + if let Some(flattened) = try_map_flattened_colored(name) { + return flattened; + } + if let Some(direct) = MODERN_DIRECT_MAP.get(name) { + return *direct; + } + if let Some(colored) = try_map_colored(name, properties) { + return colored; + } + if let Some(wood) = try_map_wood(name, properties) { + return wood; + } + if let Some(variant) = try_map_variant(name, properties) { + return variant; + } + + if let Some(ref mut blocks) = unknown_blocks { + if !name.is_empty() && name != "air" && name != "cave_air" && name != "void_air" { + blocks.push(name.to_string()); + } + } + + LegacyBlockState { id: 0, data: 0 } +} + +fn get_property<'a>(properties: Option<&'a NbtCompound>, name: &str) -> String { + properties + .and_then(|p| p.string(name)) + .unwrap_or("") + .to_string() +} + +fn get_bool_property(properties: Option<&NbtCompound>, name: &str) -> bool { + get_property(properties, name).to_lowercase() == "true" +} + +fn get_int_property(properties: Option<&NbtCompound>, name: &str, default: i32) -> i32 { + get_property(properties, name) + .parse() + .unwrap_or(default) +} + +fn try_map_fluid(name: &str, properties: Option<&NbtCompound>) -> Option { + if name != "water" && name != "lava" { + return None; + } + let level = get_int_property(properties, "level", 0).clamp(0, 15); + let is_source = level == 0; + let data = level as u8; + Some(if name == "water" { + LegacyBlockState { + id: if is_source { 9 } else { 8 }, + data, + } + } else { + LegacyBlockState { + id: if is_source { 11 } else { 10 }, + data, + } + }) +} + +fn try_map_directional(name: &str, properties: Option<&NbtCompound>) -> Option { + match name { + "ladder" => Some(LegacyBlockState { + id: 65, + data: map_ladder_facing(&get_property(properties, "facing")), + }), + "vine" => Some(LegacyBlockState { + id: 106, + data: map_vine_faces(properties), + }), + "lever" => { + let mut data = + map_lever_data(&get_property(properties, "face"), &get_property(properties, "facing")); + if get_bool_property(properties, "powered") { + data |= 8; + } + Some(LegacyBlockState { id: 69, data }) + } + _ if name.ends_with("_button") => { + let id = if is_wood_family(name) { 143 } else { 77 }; + let mut data = map_button_facing(&get_property(properties, "facing")); + if get_bool_property(properties, "powered") { + data |= 8; + } + Some(LegacyBlockState { id, data }) + } + _ if name.ends_with("_fence_gate") => { + let mut data = map_fence_gate_facing(&get_property(properties, "facing")); + if get_bool_property(properties, "open") { + data |= 4; + } + Some(LegacyBlockState { id: 107, data }) + } + _ if name.ends_with("_pressure_plate") => { + let id = match name { + "light_weighted_pressure_plate" => 147, + "heavy_weighted_pressure_plate" => 148, + _ if is_wood_family(name) => 72, + _ => 70, + }; + let powered = + get_bool_property(properties, "powered") || get_int_property(properties, "power", 0) > 0; + Some(LegacyBlockState { + id, + data: if powered { 1 } else { 0 }, + }) + } + _ if name.ends_with("_door") || name == "iron_door" => { + let id = if name == "iron_door" { 71 } else { 64 }; + let half = get_property(properties, "half"); + let is_upper = half == "upper"; + if is_upper { + let mut data = 8; + if get_property(properties, "hinge") == "right" { + data |= 1; + } + if get_bool_property(properties, "powered") { + data |= 2; + } + Some(LegacyBlockState { id, data }) + } else { + let mut data = map_door_facing(&get_property(properties, "facing")); + if get_bool_property(properties, "open") { + data |= 4; + } + Some(LegacyBlockState { id, data }) + } + } + _ if try_get_stairs_id(name).is_some() => { + let id = try_get_stairs_id(name).unwrap(); + let mut data = map_stairs_facing(&get_property(properties, "facing")); + if get_property(properties, "half") == "top" { + data |= 4; + } + Some(LegacyBlockState { id, data }) + } + _ if name.ends_with("trapdoor") => { + let mut data = map_trapdoor_facing(&get_property(properties, "facing")); + if get_bool_property(properties, "open") { + data |= 4; + } + if get_property(properties, "half") == "top" { + data |= 8; + } + Some(LegacyBlockState { id: 96, data }) + } + "dispenser" | "dropper" => { + let id = if name == "dropper" { 158 } else { 23 }; + let mut data = map_facing_data(&get_property(properties, "facing")); + if get_bool_property(properties, "triggered") { + data |= 8; + } + Some(LegacyBlockState { id, data }) + } + "piston" | "sticky_piston" => { + let id = if name == "sticky_piston" { 29 } else { 33 }; + let mut data = map_facing_data(&get_property(properties, "facing")); + if get_bool_property(properties, "extended") { + data |= 8; + } + Some(LegacyBlockState { id, data }) + } + "piston_head" => { + let mut data = map_facing_data(&get_property(properties, "facing")); + if get_property(properties, "type") == "sticky" { + data |= 8; + } + Some(LegacyBlockState { id: 34, data }) + } + "redstone_wire" => { + let data = get_int_property(properties, "power", 0).clamp(0, 15) as u8; + Some(LegacyBlockState { id: 55, data }) + } + "repeater" => { + let id = if get_bool_property(properties, "powered") { 94 } else { 93 }; + let dir = map_repeater_direction(&get_property(properties, "facing")); + let delay = get_int_property(properties, "delay", 1).clamp(1, 4); + Some(LegacyBlockState { + id, + data: dir | (((delay - 1) as u8) << 2), + }) + } + "comparator" => { + let id = if get_bool_property(properties, "powered") { 150 } else { 149 }; + let dir = map_repeater_direction(&get_property(properties, "facing")); + let mode = if get_property(properties, "mode") == "subtract" { 4 } else { 0 }; + Some(LegacyBlockState { + id, + data: dir | mode, + }) + } + "wall_torch" => Some(LegacyBlockState { + id: 50, + data: map_wall_torch_facing(&get_property(properties, "facing")), + }), + "redstone_wall_torch" => { + let id = if get_bool_property(properties, "lit") { 76 } else { 75 }; + Some(LegacyBlockState { + id, + data: map_wall_torch_facing(&get_property(properties, "facing")), + }) + } + "nether_wart" => Some(LegacyBlockState { + id: 115, + data: get_int_property(properties, "age", 0).clamp(0, 3) as u8, + }), + "wheat" => Some(LegacyBlockState { + id: 59, + data: get_int_property(properties, "age", 0).clamp(0, 7) as u8, + }), + "pumpkin_stem" | "attached_pumpkin_stem" => Some(LegacyBlockState { + id: 104, + data: if name == "attached_pumpkin_stem" { + 7 + } else { + get_int_property(properties, "age", 0).clamp(0, 7) as u8 + }, + }), + "melon_stem" | "attached_melon_stem" => Some(LegacyBlockState { + id: 105, + data: if name == "attached_melon_stem" { + 7 + } else { + get_int_property(properties, "age", 0).clamp(0, 7) as u8 + }, + }), + "cocoa" => { + let age = get_int_property(properties, "age", 0).clamp(0, 2) as u8; + let dir = map_repeater_direction(&get_property(properties, "facing")); + Some(LegacyBlockState { + id: 127, + data: (age << 2) | dir, + }) + } + "hay_block" => Some(LegacyBlockState { + id: 170, + data: match get_property(properties, "axis").as_str() { + "x" => 4, + "z" => 8, + _ => 0, + }, + }), + "quartz_pillar" => Some(LegacyBlockState { + id: 155, + data: match get_property(properties, "axis").as_str() { + "x" => 3, + "z" => 4, + _ => 2, + }, + }), + "nether_portal" => Some(LegacyBlockState { + id: 90, + data: if get_property(properties, "axis") == "z" { 2 } else { 1 }, + }), + _ => None, + } +} + +fn try_map_slab(name: &str, properties: Option<&NbtCompound>) -> Option { + if !name.ends_with("_slab") { + return None; + } + + let slab_type = get_property(properties, "type"); + let is_top = slab_type == "top"; + let is_double = slab_type == "double"; + if let Some(wood_variant) = get_wood_slab_variant(name) { + return Some(if is_double { + LegacyBlockState { + id: 125, + data: wood_variant, + } + } else { + LegacyBlockState { + id: 126, + data: wood_variant | if is_top { 8 } else { 0 }, + } + }); + } + + if let Some(slab_variant) = get_stone_slab_variant(name) { + return Some(if is_double { + LegacyBlockState { + id: 43, + data: slab_variant, + } + } else { + LegacyBlockState { + id: 44, + data: slab_variant | if is_top { 8 } else { 0 }, + } + }); + } + + Some(if is_double { + LegacyBlockState { id: 43, data: 0 } + } else { + LegacyBlockState { + id: 44, + data: if is_top { 8 } else { 0 }, + } + }) +} + +fn get_wood_slab_variant(name: &str) -> Option { + match name { + "oak_slab" => Some(0), + "spruce_slab" => Some(1), + "birch_slab" => Some(2), + "jungle_slab" => Some(3), + "acacia_slab" => Some(4), + "dark_oak_slab" => Some(5), + _ => None, + } +} + +fn get_stone_slab_variant(name: &str) -> Option { + match name { + "stone_slab" | "smooth_stone_slab" | "andesite_slab" | "polished_andesite_slab" + | "diorite_slab" | "polished_diorite_slab" | "granite_slab" | "polished_granite_slab" => { + Some(0) + } + "sandstone_slab" | "smooth_sandstone_slab" | "cut_sandstone_slab" + | "red_sandstone_slab" | "smooth_red_sandstone_slab" | "cut_red_sandstone_slab" => Some(1), + "cobblestone_slab" | "mossy_cobblestone_slab" | "cobbled_deepslate_slab" + | "deepslate_brick_slab" | "deepslate_tile_slab" => Some(3), + "brick_slab" => Some(4), + "stone_brick_slab" | "mossy_stone_brick_slab" => Some(5), + "nether_brick_slab" | "red_nether_brick_slab" => Some(6), + "quartz_slab" | "smooth_quartz_slab" | "purpur_slab" | "prismarine_slab" + | "prismarine_brick_slab" | "dark_prismarine_slab" => Some(7), + _ => None, + } +} + +fn try_get_stairs_id(name: &str) -> Option { + match name { + "oak_stairs" | "spruce_stairs" | "birch_stairs" | "jungle_stairs" | "acacia_stairs" + | "dark_oak_stairs" => Some(53), + "cobblestone_stairs" | "stone_stairs" | "mossy_cobblestone_stairs" + | "cobbled_deepslate_stairs" | "blackstone_stairs" => Some(67), + "brick_stairs" => Some(108), + "stone_brick_stairs" | "dark_prismarine_stairs" | "prismarine_stairs" + | "prismarine_brick_stairs" | "polished_blackstone_brick_stairs" + | "mossy_stone_brick_stairs" | "deepslate_brick_stairs" | "deepslate_tile_stairs" => { + Some(109) + } + "nether_brick_stairs" | "crimson_stairs" | "warped_stairs" => Some(114), + "sandstone_stairs" | "red_sandstone_stairs" => Some(128), + "quartz_stairs" | "purpur_stairs" => Some(156), + "spruce_stairs" => Some(134), + "birch_stairs" => Some(135), + "jungle_stairs" => Some(136), + "acacia_stairs" => Some(163), + "dark_oak_stairs" => Some(164), + _ => None, + } +} + +fn map_stairs_facing(facing: &str) -> u8 { + match facing { + "east" => 0, + "west" => 1, + "south" => 2, + "north" => 3, + _ => 0, + } +} + +fn map_ladder_facing(facing: &str) -> u8 { + match facing { + "north" => 2, + "south" => 3, + "west" => 4, + "east" => 5, + _ => 2, + } +} + +fn map_button_facing(facing: &str) -> u8 { + match facing { + "east" => 1, + "west" => 2, + "south" => 3, + "north" => 4, + _ => 1, + } +} + +fn map_fence_gate_facing(facing: &str) -> u8 { + match facing { + "south" => 0, + "west" => 1, + "north" => 2, + "east" => 3, + _ => 0, + } +} + +fn map_vine_faces(properties: Option<&NbtCompound>) -> u8 { + let mut data = 0; + if get_bool_property(properties, "south") { + data |= 1; + } + if get_bool_property(properties, "west") { + data |= 2; + } + if get_bool_property(properties, "north") { + data |= 4; + } + if get_bool_property(properties, "east") { + data |= 8; + } + data +} + +fn map_lever_data(face: &str, facing: &str) -> u8 { + match face { + "floor" => { + if facing == "east" || facing == "west" { + 6 + } else { + 5 + } + } + "ceiling" => { + if facing == "east" || facing == "west" { + 7 + } else { + 0 + } + } + _ => map_button_facing(facing), + } +} + +fn map_door_facing(facing: &str) -> u8 { + match facing { + "east" => 0, + "south" => 1, + "west" => 2, + "north" => 3, + _ => 0, + } +} + +fn map_trapdoor_facing(facing: &str) -> u8 { + match facing { + "north" => 0, + "south" => 1, + "west" => 2, + "east" => 3, + _ => 0, + } +} + +fn map_facing_data(facing: &str) -> u8 { + match facing { + "down" => 0, + "up" => 1, + "north" => 2, + "south" => 3, + "west" => 4, + "east" => 5, + _ => 3, + } +} + +fn map_repeater_direction(facing: &str) -> u8 { + match facing { + "south" => 0, + "west" => 1, + "north" => 2, + "east" => 3, + _ => 0, + } +} + +fn map_wall_torch_facing(facing: &str) -> u8 { + match facing { + "east" => 1, + "west" => 2, + "south" => 3, + "north" => 4, + _ => 1, + } +} + +fn try_map_colored(name: &str, properties: Option<&NbtCompound>) -> Option { + let color = get_color_data(&get_property(properties, "color")); + match name { + "wool" => Some(LegacyBlockState { id: 35, data: color }), + "stained_glass" => Some(LegacyBlockState { id: 95, data: color }), + "stained_glass_pane" => Some(LegacyBlockState { id: 160, data: color }), + "stained_hardened_clay" | "terracotta" => { + Some(LegacyBlockState { id: 159, data: color }) + } + "concrete" => Some(LegacyBlockState { + id: 172, + data: color, + }), + "concrete_powder" => Some(LegacyBlockState { id: 12, data: color }), + "glazed_terracotta" => Some(LegacyBlockState { id: 159, data: color }), + _ => None, + } +} + +fn try_map_flattened_colored(name: &str) -> Option { + let (color_data, suffix) = split_color_prefix(name)?; + let data = color_data; + match suffix.as_str() { + "wool" => Some(LegacyBlockState { id: 35, data }), + "stained_glass" | "glass" => Some(LegacyBlockState { id: 95, data }), + "stained_glass_pane" | "glass_pane" => Some(LegacyBlockState { id: 160, data }), + "stained_hardened_clay" | "terracotta" => Some(LegacyBlockState { id: 159, data }), + "concrete" => Some(LegacyBlockState { id: 172, data }), + "concrete_powder" => Some(LegacyBlockState { id: 12, data }), + "glazed_terracotta" => Some(LegacyBlockState { id: 159, data }), + "carpet" => Some(LegacyBlockState { id: 171, data }), + _ => None, + } +} + +fn split_color_prefix(name: &str) -> Option<(u8, String)> { + for color_name in COLOR_NAMES { + let prefix = format!("{}_", color_name); + if name.starts_with(&prefix) { + let suffix = name[prefix.len()..].to_string(); + return Some((get_color_data(color_name), suffix)); + } + } + None +} + +const COLOR_NAMES: &[&str] = &[ + "white", "orange", "magenta", "light_blue", "yellow", "lime", "pink", "gray", "silver", + "light_gray", "cyan", "purple", "blue", "brown", "green", "red", "black", +]; + +fn get_color_data(color: &str) -> u8 { + match color { + "white" => 0, + "orange" => 1, + "magenta" => 2, + "light_blue" => 3, + "yellow" => 4, + "lime" => 5, + "pink" => 6, + "gray" => 7, + "light_gray" => 8, + "cyan" => 9, + "purple" => 10, + "blue" => 11, + "brown" => 12, + "green" => 13, + "red" => 14, + "black" => 15, + _ => 0, + } +} + +fn try_map_wood(name: &str, properties: Option<&NbtCompound>) -> Option { + let wood_type = get_property(properties, "variant"); + let wood_type = if wood_type.is_empty() { + get_prefix_before_underscore(name) + } else { + wood_type + }; + + let data_value = match wood_type.as_str() { + "spruce" => 1, + "birch" => 2, + "jungle" => 3, + "acacia" => 0, + "dark_oak" => 1, + _ => 0, + }; + + if name.ends_with("_planks") || name == "planks" { + return Some(LegacyBlockState { id: 5, data: data_value }); + } + if name.ends_with("_sapling") || name == "sapling" { + return Some(LegacyBlockState { id: 6, data: data_value }); + } + if name.ends_with("_log") || name == "log" { + return Some(LegacyBlockState { + id: 17, + data: data_value.min(3), + }); + } + if name.ends_with("_leaves") || name == "leaves" { + return Some(LegacyBlockState { + id: 18, + data: data_value.min(3), + }); + } + if name.ends_with("_stairs") && name.contains("wood") { + return Some(LegacyBlockState { id: 53, data: 0 }); + } + if name.ends_with("_door") { + return Some(LegacyBlockState { id: 64, data: 0 }); + } + if name.ends_with("_fence") { + return Some(LegacyBlockState { id: 85, data: 0 }); + } + if name.ends_with("_fence_gate") { + return Some(LegacyBlockState { id: 107, data: 0 }); + } + if name.ends_with("_pressure_plate") && is_wood_family(name) { + return Some(LegacyBlockState { id: 72, data: 0 }); + } + + None +} + +fn try_map_variant(name: &str, properties: Option<&NbtCompound>) -> Option { + if let Some(fallback) = try_map_common_fallback(name, properties) { + return Some(fallback); + } + + if name == "redstone_lamp" { + let lit = get_bool_property(properties, "lit"); + return Some(LegacyBlockState { + id: if lit { 124 } else { 123 }, + data: 0, + }); + } + + let result = match name { + "deepslate" | "polished_deepslate" | "tuff" | "calcite" | "dripstone_block" => { + Some(LegacyBlockState { id: 1, data: 0 }) + } + "cobbled_deepslate" => Some(LegacyBlockState { id: 4, data: 0 }), + "deepslate_bricks" | "deepslate_tiles" => Some(LegacyBlockState { id: 98, data: 0 }), + "cracked_deepslate_bricks" | "cracked_deepslate_tiles" => { + Some(LegacyBlockState { id: 98, data: 2 }) + } + "chiseled_deepslate" => Some(LegacyBlockState { id: 98, data: 3 }), + "deepslate_coal_ore" => Some(LegacyBlockState { id: 16, data: 0 }), + "deepslate_iron_ore" | "deepslate_copper_ore" => { + Some(LegacyBlockState { id: 15, data: 0 }) + } + "deepslate_gold_ore" => Some(LegacyBlockState { id: 14, data: 0 }), + "deepslate_redstone_ore" => Some(LegacyBlockState { id: 73, data: 0 }), + "deepslate_lapis_ore" => Some(LegacyBlockState { id: 21, data: 0 }), + "deepslate_diamond_ore" => Some(LegacyBlockState { id: 56, data: 0 }), + "deepslate_emerald_ore" => Some(LegacyBlockState { id: 129, data: 0 }), + "deepslate_tile_stairs" | "deepslate_brick_stairs" | "cobbled_deepslate_stairs" => { + Some(LegacyBlockState { id: 67, data: 0 }) + } + "deepslate_tile_slab" | "deepslate_brick_slab" | "cobbled_deepslate_slab" => { + Some(LegacyBlockState { id: 44, data: 3 }) + } + "deepslate_tile_wall" | "deepslate_brick_wall" | "cobbled_deepslate_wall" => { + Some(LegacyBlockState { id: 139, data: 0 }) + } + "grass_block" => Some(LegacyBlockState { id: 2, data: 0 }), + "coarse_dirt" | "podzol" => Some(LegacyBlockState { id: 3, data: 0 }), + "grass_path" | "dirt_path" | "moss_carpet" => Some(LegacyBlockState { id: 2, data: 0 }), + "cobblestone_wall" => Some(LegacyBlockState { id: 139, data: 0 }), + "mossy_cobblestone_wall" => Some(LegacyBlockState { id: 139, data: 1 }), + "smooth_stone_slab" => Some(LegacyBlockState { id: 44, data: 0 }), + "stone_brick_stairs" => Some(LegacyBlockState { id: 109, data: 0 }), + "mossy_stone_bricks" => Some(LegacyBlockState { id: 98, data: 1 }), + "cracked_stone_bricks" => Some(LegacyBlockState { id: 98, data: 2 }), + "chiseled_stone_bricks" => Some(LegacyBlockState { id: 98, data: 3 }), + "red_sand" => Some(LegacyBlockState { id: 12, data: 1 }), + "red_sandstone" => Some(LegacyBlockState { id: 24, data: 0 }), + "smooth_sandstone" => Some(LegacyBlockState { id: 24, data: 2 }), + "chiseled_sandstone" | "cut_sandstone" => Some(LegacyBlockState { id: 24, data: 1 }), + "smooth_red_sandstone" | "chiseled_red_sandstone" | "cut_red_sandstone" => { + Some(LegacyBlockState { id: 24, data: 0 }) + } + "red_sandstone_stairs" => Some(LegacyBlockState { id: 128, data: 0 }), + "red_sandstone_slab" => Some(LegacyBlockState { id: 44, data: 1 }), + "prismarine" | "prismarine_bricks" | "dark_prismarine" => { + Some(LegacyBlockState { id: 1, data: 0 }) + } + "sea_lantern" => Some(LegacyBlockState { id: 89, data: 0 }), + "purpur_block" | "purpur_pillar" => Some(LegacyBlockState { id: 155, data: 0 }), + "purpur_stairs" => Some(LegacyBlockState { id: 156, data: 0 }), + "purpur_slab" => Some(LegacyBlockState { id: 44, data: 0 }), + "end_stone_bricks" => Some(LegacyBlockState { id: 121, data: 0 }), + "magma_block" => Some(LegacyBlockState { id: 87, data: 0 }), + "nether_wart_block" | "red_nether_bricks" => Some(LegacyBlockState { id: 112, data: 0 }), + "bone_block" => Some(LegacyBlockState { id: 1, data: 0 }), + "packed_ice" | "blue_ice" => Some(LegacyBlockState { id: 79, data: 0 }), + "observer" | "beetroots" | "kelp" | "kelp_plant" | "seagrass" | "tall_seagrass" + | "coral_block" | "tube_coral_block" | "brain_coral_block" | "bubble_coral_block" + | "fire_coral_block" | "horn_coral_block" | "end_rod" | "chorus_plant" + | "chorus_flower" | "end_gateway" | "structure_void" => { + Some(LegacyBlockState { id: 0, data: 0 }) + } + "sunflower" | "lilac" | "rose_bush" | "peony" => { + if get_property(properties, "half") == "upper" { + Some(LegacyBlockState { id: 0, data: 0 }) + } else { + Some(LegacyBlockState { id: 38, data: 0 }) + } + } + "tall_grass" => { + if get_property(properties, "half") == "upper" { + Some(LegacyBlockState { id: 0, data: 0 }) + } else { + let grass_type = get_property(properties, "type"); + Some(LegacyBlockState { + id: 31, + data: if grass_type == "fern" { 2 } else { 1 }, + }) + } + } + "large_fern" => { + if get_property(properties, "half") == "upper" { + Some(LegacyBlockState { id: 0, data: 0 }) + } else { + Some(LegacyBlockState { id: 31, data: 2 }) + } + } + "stone" => { + let _type = get_property(properties, "type"); + Some(LegacyBlockState { id: 1, data: 0 }) + } + "sandstone" => { + let stype = get_property(properties, "type"); + Some(LegacyBlockState { + id: 24, + data: match stype.as_str() { + "chiseled" => 1, + "smooth" => 2, + _ => 0, + }, + }) + } + "stone_bricks" => { + let stype = get_property(properties, "type"); + Some(LegacyBlockState { + id: 98, + data: match stype.as_str() { + "mossy" => 1, + "cracked" => 2, + "chiseled" => 3, + _ => 0, + }, + }) + } + _ => None, + }; + + result +} + +fn try_map_common_fallback(name: &str, properties: Option<&NbtCompound>) -> Option { + if name.ends_with("_button") { + return Some(LegacyBlockState { + id: if is_wood_family(name) { 143 } else { 77 }, + data: 0, + }); + } + if name.ends_with("_wall_sign") { + return Some(LegacyBlockState { + id: 68, + data: map_wall_sign_facing(&get_property(properties, "facing")), + }); + } + if name.ends_with("_sign") { + return Some(LegacyBlockState { + id: 63, + data: (get_int_property(properties, "rotation", 0) & 0x0F) as u8, + }); + } + if name.ends_with("_wall") { + return Some(LegacyBlockState { id: 139, data: 0 }); + } + if name.ends_with("_bed") { + let mut data = map_bed_facing(&get_property(properties, "facing")); + if get_property(properties, "part") == "head" { + data |= 8; + } + return Some(LegacyBlockState { id: 26, data }); + } + if name.ends_with("_banner") { + return Some(LegacyBlockState { id: 0, data: 0 }); + } + + match name { + "barrier" | "structure_void" => Some(LegacyBlockState { id: 0, data: 0 }), + "bamboo" => Some(LegacyBlockState { id: 83, data: 0 }), + "cake" => Some(LegacyBlockState { id: 92, data: 0 }), + "brewing_stand" => Some(LegacyBlockState { id: 117, data: 0 }), + "barrel" => Some(LegacyBlockState { id: 54, data: 0 }), + "blast_furnace" => Some(LegacyBlockState { id: 61, data: 0 }), + "campfire" => Some(LegacyBlockState { id: 50, data: 0 }), + "azure_bluet" | "blue_orchid" => Some(LegacyBlockState { id: 38, data: 0 }), + "attached_melon_stem" => Some(LegacyBlockState { id: 105, data: 7 }), + "attached_pumpkin_stem" => Some(LegacyBlockState { id: 104, data: 7 }), + "andesite" | "diorite" | "granite" | "polished_andesite" | "polished_diorite" + | "polished_granite" => Some(LegacyBlockState { id: 1, data: 0 }), + "andesite_stairs" | "diorite_stairs" | "granite_stairs" | "polished_andesite_stairs" + | "polished_diorite_stairs" | "polished_granite_stairs" => { + Some(LegacyBlockState { id: 109, data: 0 }) + } + "bubble_column" => Some(LegacyBlockState { id: 9, data: 0 }), + "amethyst_block" | "budding_amethyst" | "amethyst_cluster" => { + Some(LegacyBlockState { id: 20, data: 0 }) + } + _ => None, + } +} + +fn map_bed_facing(facing: &str) -> u8 { + match facing { + "south" => 0, + "west" => 1, + "north" => 2, + "east" => 3, + _ => 0, + } +} + +fn map_wall_sign_facing(facing: &str) -> u8 { + match facing { + "north" => 2, + "south" => 3, + "west" => 4, + "east" => 5, + _ => 2, + } +} + +fn get_prefix_before_underscore(value: &str) -> String { + let index = value.find('_'); + match index { + Some(i) if i > 0 => value[..i].to_string(), + _ => value.to_string(), + } +} + +fn is_wood_family(name: &str) -> bool { + name.contains("oak") + || name.contains("spruce") + || name.contains("birch") + || name.contains("jungle") + || name.contains("acacia") + || name.contains("dark_oak") + || name.contains("mangrove") + || name.contains("cherry") + || name.contains("bamboo") + || name.contains("crimson") + || name.contains("warped") +} + +pub fn is_valid_lce_tile_id(id: u8) -> bool { + id <= 160 || (id >= 170 && id <= 173) +} + +pub fn remap_blocks(blocks: &mut [u8], _data: &mut [u8]) { + for id in blocks.iter_mut() { + let mut bid = *id; + if bid == 137 { + bid = 1; + } + if bid >= 165 { + if let Some(&replacement) = BLOCK_REMAP_TABLE.get(&bid) { + bid = replacement; + } + } + if !is_valid_lce_tile_id(bid) { + bid = 0; + } + *id = bid; + } +} + +static MODERN_DIRECT_MAP: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + let mut m = HashMap::new(); + m.insert("air", LegacyBlockState { id: 0, data: 0 }); + m.insert("cave_air", LegacyBlockState { id: 0, data: 0 }); + m.insert("void_air", LegacyBlockState { id: 0, data: 0 }); + m.insert("stone", LegacyBlockState { id: 1, data: 0 }); + m.insert("grass", LegacyBlockState { id: 31, data: 1 }); + m.insert("short_grass", LegacyBlockState { id: 31, data: 1 }); + m.insert("short_dry_grass", LegacyBlockState { id: 31, data: 1 }); + m.insert("dirt", LegacyBlockState { id: 3, data: 0 }); + m.insert("cobblestone", LegacyBlockState { id: 4, data: 0 }); + m.insert("bedrock", LegacyBlockState { id: 7, data: 0 }); + m.insert("water", LegacyBlockState { id: 8, data: 0 }); + m.insert("lava", LegacyBlockState { id: 10, data: 0 }); + m.insert("sand", LegacyBlockState { id: 12, data: 0 }); + m.insert("gravel", LegacyBlockState { id: 13, data: 0 }); + m.insert("gold_ore", LegacyBlockState { id: 14, data: 0 }); + m.insert("iron_ore", LegacyBlockState { id: 15, data: 0 }); + m.insert("coal_ore", LegacyBlockState { id: 16, data: 0 }); + m.insert("oak_log", LegacyBlockState { id: 17, data: 0 }); + m.insert("oak_leaves", LegacyBlockState { id: 18, data: 0 }); + m.insert("glass", LegacyBlockState { id: 20, data: 0 }); + m.insert("lapis_ore", LegacyBlockState { id: 21, data: 0 }); + m.insert("lapis_block", LegacyBlockState { id: 22, data: 0 }); + m.insert("dispenser", LegacyBlockState { id: 23, data: 0 }); + m.insert("sandstone", LegacyBlockState { id: 24, data: 0 }); + m.insert("note_block", LegacyBlockState { id: 25, data: 0 }); + m.insert("powered_rail", LegacyBlockState { id: 27, data: 0 }); + m.insert("detector_rail", LegacyBlockState { id: 28, data: 0 }); + m.insert("sticky_piston", LegacyBlockState { id: 29, data: 0 }); + m.insert("cobweb", LegacyBlockState { id: 30, data: 0 }); + m.insert("dead_bush", LegacyBlockState { id: 32, data: 0 }); + m.insert("piston", LegacyBlockState { id: 33, data: 0 }); + m.insert("wool", LegacyBlockState { id: 35, data: 0 }); + m.insert("dandelion", LegacyBlockState { id: 37, data: 0 }); + m.insert("poppy", LegacyBlockState { id: 38, data: 0 }); + m.insert("brown_mushroom", LegacyBlockState { id: 39, data: 0 }); + m.insert("red_mushroom", LegacyBlockState { id: 40, data: 0 }); + m.insert("gold_block", LegacyBlockState { id: 41, data: 0 }); + m.insert("iron_block", LegacyBlockState { id: 42, data: 0 }); + m.insert("stone_slab", LegacyBlockState { id: 44, data: 0 }); + m.insert("bricks", LegacyBlockState { id: 45, data: 0 }); + m.insert("tnt", LegacyBlockState { id: 46, data: 0 }); + m.insert("bookshelf", LegacyBlockState { id: 47, data: 0 }); + m.insert("mossy_cobblestone", LegacyBlockState { id: 48, data: 0 }); + m.insert("obsidian", LegacyBlockState { id: 49, data: 0 }); + m.insert("torch", LegacyBlockState { id: 50, data: 0 }); + m.insert("fire", LegacyBlockState { id: 51, data: 0 }); + m.insert("mob_spawner", LegacyBlockState { id: 52, data: 0 }); + m.insert("oak_stairs", LegacyBlockState { id: 53, data: 0 }); + m.insert("chest", LegacyBlockState { id: 54, data: 0 }); + m.insert("diamond_ore", LegacyBlockState { id: 56, data: 0 }); + m.insert("diamond_block", LegacyBlockState { id: 57, data: 0 }); + m.insert("crafting_table", LegacyBlockState { id: 58, data: 0 }); + m.insert("farmland", LegacyBlockState { id: 60, data: 0 }); + m.insert("furnace", LegacyBlockState { id: 61, data: 0 }); + m.insert("ladder", LegacyBlockState { id: 65, data: 0 }); + m.insert("rail", LegacyBlockState { id: 66, data: 0 }); + m.insert("lever", LegacyBlockState { id: 69, data: 0 }); + m.insert("stone_pressure_plate", LegacyBlockState { id: 70, data: 0 }); + m.insert("oak_pressure_plate", LegacyBlockState { id: 72, data: 0 }); + m.insert("redstone_ore", LegacyBlockState { id: 73, data: 0 }); + m.insert("redstone_torch", LegacyBlockState { id: 76, data: 0 }); + m.insert("stone_button", LegacyBlockState { id: 77, data: 0 }); + m.insert("snow", LegacyBlockState { id: 78, data: 0 }); + m.insert("ice", LegacyBlockState { id: 79, data: 0 }); + m.insert("snow_block", LegacyBlockState { id: 80, data: 0 }); + m.insert("cactus", LegacyBlockState { id: 81, data: 0 }); + m.insert("clay", LegacyBlockState { id: 82, data: 0 }); + m.insert("jukebox", LegacyBlockState { id: 84, data: 0 }); + m.insert("oak_fence", LegacyBlockState { id: 85, data: 0 }); + m.insert("netherrack", LegacyBlockState { id: 87, data: 0 }); + m.insert("soul_sand", LegacyBlockState { id: 88, data: 0 }); + m.insert("glowstone", LegacyBlockState { id: 89, data: 0 }); + m.insert("jack_o_lantern", LegacyBlockState { id: 91, data: 0 }); + m.insert("stone_bricks", LegacyBlockState { id: 98, data: 0 }); + m.insert("brown_mushroom_block", LegacyBlockState { id: 99, data: 0 }); + m.insert("red_mushroom_block", LegacyBlockState { id: 100, data: 0 }); + m.insert("iron_bars", LegacyBlockState { id: 101, data: 0 }); + m.insert("glass_pane", LegacyBlockState { id: 102, data: 0 }); + m.insert("melon", LegacyBlockState { id: 103, data: 0 }); + m.insert("vine", LegacyBlockState { id: 106, data: 0 }); + m.insert("oak_fence_gate", LegacyBlockState { id: 107, data: 0 }); + m.insert("brick_stairs", LegacyBlockState { id: 108, data: 0 }); + m.insert("stone_brick_stairs", LegacyBlockState { id: 109, data: 0 }); + m.insert("mycelium", LegacyBlockState { id: 110, data: 0 }); + m.insert("lily_pad", LegacyBlockState { id: 111, data: 0 }); + m.insert("nether_bricks", LegacyBlockState { id: 112, data: 0 }); + m.insert("nether_brick_fence", LegacyBlockState { id: 113, data: 0 }); + m.insert("nether_brick_stairs", LegacyBlockState { id: 114, data: 0 }); + m.insert("enchanting_table", LegacyBlockState { id: 116, data: 0 }); + m.insert("end_stone", LegacyBlockState { id: 121, data: 0 }); + m.insert("sandstone_stairs", LegacyBlockState { id: 128, data: 0 }); + m.insert("emerald_ore", LegacyBlockState { id: 129, data: 0 }); + m.insert("ender_chest", LegacyBlockState { id: 130, data: 0 }); + m.insert("tripwire_hook", LegacyBlockState { id: 131, data: 0 }); + m.insert("emerald_block", LegacyBlockState { id: 133, data: 0 }); + m.insert("spruce_stairs", LegacyBlockState { id: 134, data: 0 }); + m.insert("birch_stairs", LegacyBlockState { id: 135, data: 0 }); + m.insert("jungle_stairs", LegacyBlockState { id: 136, data: 0 }); + m.insert("command_block", LegacyBlockState { id: 1, data: 0 }); + m.insert("beacon", LegacyBlockState { id: 138, data: 0 }); + m.insert("cobblestone_wall", LegacyBlockState { id: 139, data: 0 }); + m.insert("flower_pot", LegacyBlockState { id: 140, data: 0 }); + m.insert("carrots", LegacyBlockState { id: 141, data: 0 }); + m.insert("potatoes", LegacyBlockState { id: 142, data: 0 }); + m.insert("oak_button", LegacyBlockState { id: 143, data: 0 }); + m.insert("anvil", LegacyBlockState { id: 145, data: 0 }); + m.insert("trapped_chest", LegacyBlockState { id: 146, data: 0 }); + m.insert("light_weighted_pressure_plate", LegacyBlockState { id: 147, data: 0 }); + m.insert("heavy_weighted_pressure_plate", LegacyBlockState { id: 148, data: 0 }); + m.insert("daylight_detector", LegacyBlockState { id: 151, data: 0 }); + m.insert("redstone_block", LegacyBlockState { id: 152, data: 0 }); + m.insert("quartz_ore", LegacyBlockState { id: 153, data: 0 }); + m.insert("hopper", LegacyBlockState { id: 154, data: 0 }); + m.insert("quartz_block", LegacyBlockState { id: 155, data: 0 }); + m.insert("quartz_stairs", LegacyBlockState { id: 156, data: 0 }); + m.insert("activator_rail", LegacyBlockState { id: 157, data: 0 }); + m.insert("dropper", LegacyBlockState { id: 158, data: 0 }); + m.insert("stained_hardened_clay", LegacyBlockState { id: 159, data: 0 }); + m.insert("stained_glass", LegacyBlockState { id: 95, data: 0 }); + m.insert("stained_glass_pane", LegacyBlockState { id: 160, data: 0 }); + m.insert("leaves2", LegacyBlockState { id: 161, data: 0 }); + m.insert("log2", LegacyBlockState { id: 162, data: 0 }); + m.insert("acacia_stairs", LegacyBlockState { id: 163, data: 0 }); + m.insert("dark_oak_stairs", LegacyBlockState { id: 164, data: 0 }); + m.insert("fern", LegacyBlockState { id: 31, data: 2 }); + m.insert("sugar_cane", LegacyBlockState { id: 83, data: 0 }); + m.insert("pumpkin", LegacyBlockState { id: 86, data: 0 }); + m.insert("carved_pumpkin", LegacyBlockState { id: 86, data: 0 }); + m.insert("nether_quartz_ore", LegacyBlockState { id: 153, data: 0 }); + m.insert("dragon_egg", LegacyBlockState { id: 122, data: 0 }); + m.insert("spawner", LegacyBlockState { id: 52, data: 0 }); + m.insert("tripwire", LegacyBlockState { id: 132, data: 0 }); + m.insert("sponge", LegacyBlockState { id: 19, data: 0 }); + m.insert("wet_sponge", LegacyBlockState { id: 19, data: 0 }); + m.insert("cauldron", LegacyBlockState { id: 118, data: 0 }); + m.insert("water_cauldron", LegacyBlockState { id: 118, data: 3 }); + m.insert("coal_block", LegacyBlockState { id: 173, data: 0 }); + m.insert("mushroom_stem", LegacyBlockState { id: 99, data: 10 }); + m.insert("smooth_stone", LegacyBlockState { id: 1, data: 0 }); + m.insert("infested_stone", LegacyBlockState { id: 1, data: 0 }); + m.insert("infested_stone_bricks", LegacyBlockState { id: 98, data: 0 }); + m.insert("infested_deepslate", LegacyBlockState { id: 1, data: 0 }); + m.insert( + "polished_blackstone_bricks", + LegacyBlockState { id: 98, data: 0 }, + ); + m.insert( + "cracked_polished_blackstone_bricks", + LegacyBlockState { id: 98, data: 2 }, + ); + m.insert("blackstone", LegacyBlockState { id: 4, data: 0 }); + m.insert("quartz_bricks", LegacyBlockState { id: 155, data: 0 }); + m.insert( + "chiseled_quartz_block", + LegacyBlockState { id: 155, data: 1 }, + ); + m.insert( + "chiseled_nether_bricks", + LegacyBlockState { id: 112, data: 0 }, + ); + m.insert("smooth_basalt", LegacyBlockState { id: 1, data: 0 }); + m.insert("pointed_dripstone", LegacyBlockState { id: 1, data: 0 }); + m.insert("lodestone", LegacyBlockState { id: 1, data: 0 }); + m.insert("structure_block", LegacyBlockState { id: 1, data: 0 }); + m.insert("stonecutter", LegacyBlockState { id: 4, data: 0 }); + m.insert("rooted_dirt", LegacyBlockState { id: 3, data: 0 }); + m.insert("moss_block", LegacyBlockState { id: 48, data: 0 }); + m.insert( + "dried_kelp_block", + LegacyBlockState { id: 170, data: 0 }, + ); + m.insert("crimson_stem", LegacyBlockState { id: 17, data: 0 }); + m.insert("warped_stem", LegacyBlockState { id: 17, data: 0 }); + m.insert( + "stripped_crimson_stem", + LegacyBlockState { id: 17, data: 0 }, + ); + m.insert( + "stripped_warped_stem", + LegacyBlockState { id: 17, data: 0 }, + ); + m.insert("azalea", LegacyBlockState { id: 18, data: 0 }); + m.insert("flowering_azalea", LegacyBlockState { id: 18, data: 0 }); + m.insert( + "big_dripleaf", + LegacyBlockState { id: 111, data: 0 }, + ); + m.insert( + "big_dripleaf_stem", + LegacyBlockState { id: 17, data: 0 }, + ); + m.insert("small_dripleaf", LegacyBlockState { id: 0, data: 0 }); + m.insert("hanging_roots", LegacyBlockState { id: 0, data: 0 }); + m.insert("spore_blossom", LegacyBlockState { id: 38, data: 0 }); + m.insert( + "twisting_vines", + LegacyBlockState { id: 106, data: 0 }, + ); + m.insert( + "twisting_vines_plant", + LegacyBlockState { id: 106, data: 0 }, + ); + m.insert("cave_vines", LegacyBlockState { id: 0, data: 0 }); + m.insert( + "cave_vines_plant", + LegacyBlockState { id: 0, data: 0 }, + ); + m.insert("glow_lichen", LegacyBlockState { id: 0, data: 0 }); + m.insert("cornflower", LegacyBlockState { id: 38, data: 0 }); + m.insert( + "lily_of_the_valley", + LegacyBlockState { id: 38, data: 0 }, + ); + m.insert("oxeye_daisy", LegacyBlockState { id: 38, data: 0 }); + m.insert("pink_tulip", LegacyBlockState { id: 38, data: 0 }); + m.insert("brain_coral", LegacyBlockState { id: 38, data: 0 }); + m.insert("bubble_coral", LegacyBlockState { id: 38, data: 0 }); + m.insert("fire_coral", LegacyBlockState { id: 38, data: 0 }); + m.insert("horn_coral", LegacyBlockState { id: 38, data: 0 }); + m.insert("tube_coral", LegacyBlockState { id: 38, data: 0 }); + m.insert( + "horn_coral_fan", + LegacyBlockState { id: 38, data: 0 }, + ); + m.insert( + "tube_coral_fan", + LegacyBlockState { id: 38, data: 0 }, + ); + m.insert("smoker", LegacyBlockState { id: 61, data: 0 }); + m.insert( + "grindstone", + LegacyBlockState { id: 145, data: 0 }, + ); + m.insert("bell", LegacyBlockState { id: 145, data: 0 }); + m.insert("loom", LegacyBlockState { id: 58, data: 0 }); + m.insert( + "smithing_table", + LegacyBlockState { id: 58, data: 0 }, + ); + m.insert( + "target", + LegacyBlockState { id: 70, data: 0 }, + ); + m.insert("chain", LegacyBlockState { id: 101, data: 0 }); + m.insert( + "lightning_rod", + LegacyBlockState { id: 101, data: 0 }, + ); + m.insert("lantern", LegacyBlockState { id: 50, data: 0 }); + m.insert( + "shroomlight", + LegacyBlockState { id: 89, data: 0 }, + ); + m.insert( + "soul_campfire", + LegacyBlockState { id: 51, data: 0 }, + ); + m.insert( + "bee_nest", + LegacyBlockState { id: 54, data: 0 }, + ); + m.insert( + "wither_skeleton_skull", + LegacyBlockState { id: 144, data: 0 }, + ); + m.insert( + "dragon_wall_head", + LegacyBlockState { id: 144, data: 0 }, + ); + m.insert( + "nether_portal", + LegacyBlockState { id: 90, data: 0 }, + ); + m.insert( + "white_candle", + LegacyBlockState { id: 50, data: 0 }, + ); + m.insert( + "orange_candle", + LegacyBlockState { id: 50, data: 0 }, + ); + m.insert( + "gray_candle", + LegacyBlockState { id: 50, data: 0 }, + ); + m.insert( + "cyan_candle", + LegacyBlockState { id: 50, data: 0 }, + ); + m.insert( + "lime_candle", + LegacyBlockState { id: 50, data: 0 }, + ); + m.insert( + "potted_blue_orchid", + LegacyBlockState { id: 140, data: 0 }, + ); + m.insert( + "potted_brown_mushroom", + LegacyBlockState { id: 140, data: 0 }, + ); + m.insert( + "potted_cactus", + LegacyBlockState { id: 140, data: 0 }, + ); + m.insert( + "potted_dandelion", + LegacyBlockState { id: 140, data: 0 }, + ); + m.insert( + "potted_oxeye_daisy", + LegacyBlockState { id: 140, data: 0 }, + ); + m.insert( + "potted_poppy", + LegacyBlockState { id: 140, data: 0 }, + ); + m.insert( + "potted_red_mushroom", + LegacyBlockState { id: 140, data: 0 }, + ); + m.insert( + "powder_snow", + LegacyBlockState { id: 80, data: 0 }, + ); + m.insert( + "copper_ore", + LegacyBlockState { id: 15, data: 0 }, + ); + m.insert( + "oxidized_copper", + LegacyBlockState { id: 15, data: 0 }, + ); + m.insert( + "weathered_copper", + LegacyBlockState { id: 15, data: 0 }, + ); + m.insert( + "raw_copper_block", + LegacyBlockState { id: 42, data: 0 }, + ); + m.insert( + "raw_iron_block", + LegacyBlockState { id: 42, data: 0 }, + ); + m.insert( + "small_amethyst_bud", + LegacyBlockState { id: 20, data: 0 }, + ); + m.insert( + "medium_amethyst_bud", + LegacyBlockState { id: 20, data: 0 }, + ); + m.insert( + "large_amethyst_bud", + LegacyBlockState { id: 20, data: 0 }, + ); + m.insert( + "slime_block", + LegacyBlockState { id: 0, data: 0 }, + ); + m.insert("light", LegacyBlockState { id: 0, data: 0 }); + m.insert( + "scaffolding", + LegacyBlockState { id: 0, data: 0 }, + ); + m.insert( + "powder_snow_cauldron", + LegacyBlockState { id: 118, data: 0 }, + ); + m.insert( + "lava_cauldron", + LegacyBlockState { id: 118, data: 0 }, + ); + m + }); + +static BLOCK_REMAP_TABLE: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + let mut m = HashMap::new(); + m.insert(174, 79); + m.insert(175, 0); + m.insert(165, 0); + m.insert(166, 0); + m.insert(167, 96); + m.insert(168, 1); + m.insert(169, 89); + m.insert(176, 0); + m.insert(177, 0); + m.insert(178, 151); + m.insert(179, 24); + m.insert(180, 128); + m.insert(181, 43); + m.insert(182, 44); + m.insert(183, 107); + m.insert(184, 107); + m.insert(185, 107); + m.insert(186, 107); + m.insert(187, 107); + m.insert(188, 85); + m.insert(189, 85); + m.insert(190, 85); + m.insert(191, 85); + m.insert(192, 85); + m.insert(193, 64); + m.insert(194, 64); + m.insert(195, 64); + m.insert(196, 64); + m.insert(197, 64); + m.insert(198, 0); + m.insert(199, 0); + m.insert(200, 0); + m.insert(201, 155); + m.insert(202, 155); + m.insert(203, 156); + m.insert(204, 43); + m.insert(205, 44); + m.insert(206, 121); + m.insert(207, 0); + m.insert(208, 2); + m.insert(209, 0); + m.insert(210, 1); + m.insert(211, 1); + m.insert(212, 79); + m.insert(213, 87); + m.insert(214, 112); + m.insert(215, 112); + m.insert(216, 1); + m.insert(217, 0); + m.insert(218, 0); + for i in 219..=234 { + m.insert(i, 0); + } + for i in 235..=250 { + m.insert(i, 159); + } + m.insert(251, 172); + m.insert(252, 12); + m + }); diff --git a/src-tauri/src/commands/java2lce/chunk.rs b/src-tauri/src/commands/java2lce/chunk.rs new file mode 100644 index 0000000..e279b98 --- /dev/null +++ b/src-tauri/src/commands/java2lce/chunk.rs @@ -0,0 +1,1690 @@ +use std::collections::HashMap; +use super::block_mapping::{self, LegacyBlockState}; +use super::nbt; +use super::payload; +const CHUNK_BLOCKS: usize = 65536; +const CHUNK_NIBBLES: usize = 32768; +const HEIGHTMAP_SIZE: usize = 256; +const BIOMES_SIZE: usize = 256; +const STAIR_UPSIDE_DOWN_BIT: u8 = 4; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum JavaChunkFormat { + Unknown, + LegacyBlockArray, + LegacyAnvil, + ModernPalette, + ModernExtendedHeight, +} + +#[derive(Debug, Clone, Copy)] +pub struct JavaChunkFormatInfo { + pub format: JavaChunkFormat, + pub has_level_wrapper: bool, + pub data_version: i32, + pub min_section_y: Option, + pub max_section_y: Option, + pub has_modern_entity_tags: bool, +} + +impl JavaChunkFormatInfo { + pub fn is_section_based(&self) -> bool { + matches!( + self.format, + JavaChunkFormat::LegacyAnvil + | JavaChunkFormat::ModernPalette + | JavaChunkFormat::ModernExtendedHeight + ) + } + + pub fn uses_palette_sections(&self) -> bool { + matches!( + self.format, + JavaChunkFormat::ModernPalette | JavaChunkFormat::ModernExtendedHeight + ) + } + + pub fn uses_modern_content_schema(&self) -> bool { + self.uses_palette_sections() || self.has_modern_entity_tags + } + + pub fn requires_section_shift(&self) -> bool { + self.format == JavaChunkFormat::ModernExtendedHeight + } +} + +pub fn inspect_chunk(root_tag: &nbt::NbtCompound) -> JavaChunkFormatInfo { + let has_level_wrapper = root_tag.get("Level").is_some(); + let source_level = root_tag + .compound("Level") + .unwrap_or(root_tag); + + let data_version = root_tag + .int("DataVersion") + .or_else(|| { + root_tag + .compound("Level") + .and_then(|l| l.int("DataVersion")) + }) + .unwrap_or(0); + + let has_modern_entity_tags = source_level.contains("block_entities") || source_level.contains("entities"); + let has_legacy_block_arrays = source_level.contains("Blocks"); + let sections = source_level + .list("Sections") + .or_else(|| source_level.list("sections")); + + let mut has_sections = false; + let mut has_palette_sections = false; + let mut min_section_y: Option = None; + let mut max_section_y: Option = None; + if let Some(sects) = sections { + has_sections = !sects.is_empty(); + for tag in sects { + if let nbt::NbtValue::Compound(section) = tag { + if let Some(sy) = read_section_y(section) { + min_section_y = Some(min_section_y.map_or(sy, |v| v.min(sy))); + max_section_y = Some(max_section_y.map_or(sy, |v| v.max(sy))); + } + if section_uses_palette_storage(section) { + has_palette_sections = true; + } + } + } + } + + let format = if has_palette_sections { + let has_extended = min_section_y.map_or(false, |v| v < 0) + || max_section_y.map_or(false, |v| v > 15); + if has_extended { + JavaChunkFormat::ModernExtendedHeight + } else { + JavaChunkFormat::ModernPalette + } + } else if has_sections { + JavaChunkFormat::LegacyAnvil + } else if has_legacy_block_arrays { + JavaChunkFormat::LegacyBlockArray + } else { + JavaChunkFormat::Unknown + }; + + JavaChunkFormatInfo { + format, + has_level_wrapper, + data_version, + min_section_y, + max_section_y, + has_modern_entity_tags, + } +} + +pub fn read_section_y(section: &nbt::NbtCompound) -> Option { + if let Some(b) = section.byte("Y") { + return Some(b as i8 as i32); + } + if let Some(i) = section.int("Y") { + return Some(i); + } + if let Some(s) = section.short("Y") { + return Some(s as i32); + } + None +} + +fn section_uses_palette_storage(section: &nbt::NbtCompound) -> bool { + section.contains("block_states") + || section.contains("Palette") + || section.contains("BlockStates") +} + +fn get_byte_array_or_default(compound: &nbt::NbtCompound, name: &str, default_size: usize) -> Vec { + match compound.byte_array(name) { + Some(bytes) => { + let mut v = bytes.to_vec(); + if v.len() < default_size { + v.resize(default_size, 0); + } else { + v.truncate(default_size); + } + v + } + None => vec![0u8; default_size], + } +} + +struct DecodedSection { + section_y: i32, + blocks: Vec, + data: Vec, + sky_light: Option>, + block_light: Option>, + non_air_count: u32, +} + +fn count_non_air(blocks: &[u8]) -> u32 { + blocks.iter().filter(|&&b| b != 0).count() as u32 +} + +fn try_decode_section_blocks( + section: &nbt::NbtCompound, +) -> Option<(Vec, Vec)> { + if let Some(old_blocks) = section.byte_array("Blocks") { + if old_blocks.len() >= 4096 { + let blocks = old_blocks[..4096].to_vec(); + let data = section + .byte_array("Data") + .map(|d| d.to_vec()) + .unwrap_or_else(|| vec![0u8; CHUNK_NIBBLES]); + return Some((blocks, data)); + } + } + try_decode_palette_section(section) +} + +fn try_decode_palette_section(section: &nbt::NbtCompound) -> Option<(Vec, Vec)> { + let mut blocks = vec![0u8; 4096]; + let mut data = vec![0u8; 2048]; + let block_states_container = section.compound("block_states"); + let palette = section + .list("Palette") + .or_else(|| { + block_states_container + .and_then(|bs| bs.list("palette")) + }); + + let palette = palette?; + if palette.is_empty() { + return None; + } + + if palette.len() == 1 { + if let nbt::NbtValue::Compound(entry) = &palette[0] { + let lb = block_mapping::map_modern_block_state(entry); + blocks.fill(lb.id); + if lb.data != 0 { + for i in 0..4096 { + nbt::set_nibble(&mut data, i, lb.data); + } + } + return Some((blocks, data)); + } + return None; + } + + let block_states_tag = section + .long_array("BlockStates") + .or_else(|| { + block_states_container + .and_then(|bs| bs.long_array("data")) + })?; + + if block_states_tag.is_empty() { + return None; + } + + let bits_per_block = get_bits_required(palette.len() - 1).max(4); + let values_per_long = (64 / bits_per_block).max(1); + let expected_long_count = (4096 + values_per_long - 1) / values_per_long; + let use_padded = block_states_tag.len() == expected_long_count; + for i in 0..4096 { + let palette_index = if use_padded { + read_padded_block_state(block_states_tag, bits_per_block, i) + } else { + read_compact_block_state(block_states_tag, bits_per_block, i) + }; + + if (palette_index as usize) >= palette.len() { + continue; + } + + if let nbt::NbtValue::Compound(entry) = &palette[palette_index] { + let legacy = block_mapping::map_modern_block_state(entry); + blocks[i] = legacy.id; + if legacy.data != 0 { + nbt::set_nibble(&mut data, i, legacy.data); + } + } + } + + Some((blocks, data)) +} + +fn get_bits_required(value: usize) -> usize { + let mut bits = 0; + let mut v = value; + while v > 0 { + bits += 1; + v >>= 1; + } + bits.max(1) +} + +fn read_padded_block_state(block_states: &[i64], bits_per_block: usize, index: usize) -> usize { + let values_per_long = (64 / bits_per_block).max(1); + let long_index = index / values_per_long; + let bit_offset = (index % values_per_long) * bits_per_block; + let mask = (1u64 << bits_per_block) - 1; + ((block_states[long_index] as u64 >> bit_offset) & mask) as usize +} + +fn read_compact_block_state(block_states: &[i64], bits_per_block: usize, index: usize) -> usize { + let start_bit = index * bits_per_block; + let long_index = start_bit >> 6; + let bit_offset = start_bit & 63; + let mask = (1u64 << bits_per_block) - 1; + let mut value = block_states[long_index] as u64 >> bit_offset; + let bits_read = 64 - bit_offset; + if bits_read < bits_per_block && long_index + 1 < block_states.len() { + value |= (block_states[long_index + 1] as u64) << bits_read; + } + (value & mask) as usize +} + +fn flatten_anvil_sections( + level: &nbt::NbtCompound, + format_info: &JavaChunkFormatInfo, + global_section_shift: &mut Option, +) -> (Vec, Vec, Vec, Vec) { + let mut blocks = vec![0u8; CHUNK_BLOCKS]; + let mut data = vec![0u8; CHUNK_NIBBLES]; + let mut sky_light = vec![0xFFu8; CHUNK_NIBBLES]; + let mut block_light = vec![0u8; CHUNK_NIBBLES]; + let sections = level + .list("Sections") + .or_else(|| level.list("sections")); + + let sections = match sections { + Some(s) => s, + None => return (blocks, data, sky_light, block_light), + }; + + let mut decoded_sections: Vec = Vec::new(); + for tag in sections { + if let nbt::NbtValue::Compound(section) = tag { + let section_y = match read_section_y(section) { + Some(y) => y, + None => continue, + }; + + let (s_blocks, s_data) = match try_decode_section_blocks(section) { + Some(v) => v, + None => continue, + }; + + let s_sky = section + .byte_array("SkyLight") + .or_else(|| section.byte_array("sky_light")) + .map(|b| b.to_vec()); + let s_block = section + .byte_array("BlockLight") + .or_else(|| section.byte_array("block_light")) + .map(|b| b.to_vec()); + + let non_air = count_non_air(&s_blocks); + decoded_sections.push(DecodedSection { + section_y, + blocks: s_blocks, + data: s_data, + sky_light: s_sky, + block_light: s_block, + non_air_count: non_air, + }); + } + } + + if decoded_sections.is_empty() { + return (blocks, data, sky_light, block_light); + } + + let mut section_shift = 0; + if format_info.requires_section_shift() { + if global_section_shift.is_none() { + let anchor = decoded_sections + .iter() + .max_by_key(|s| (s.non_air_count, -(s.section_y - 4).abs())) + .map(|s| s.section_y) + .unwrap_or(4); + *global_section_shift = Some(anchor - 4); + } + section_shift = global_section_shift.unwrap_or(0); + } + + for section in &decoded_sections { + let remapped_y = section.section_y - section_shift; + if remapped_y < 0 || remapped_y > 15 { + continue; + } + + let base_y = remapped_y * 16; + for i in 0..4096 { + let x = i & 0x0F; + let z = (i >> 4) & 0x0F; + let y = (i >> 8) & 0x0F; + let global_y = base_y + y as i32; + if global_y < 0 || global_y >= 256 { + continue; + } + let flat_index = ((x * 16) + z) * 256 + global_y as usize; + if flat_index < CHUNK_BLOCKS { + blocks[flat_index] = section.blocks[i]; + if section.data.len() > i { + nbt::set_nibble(&mut data, flat_index, nbt::get_nibble(§ion.data, i)); + } + if let Some(ref sky) = section.sky_light { + if sky.len() > i { + nbt::set_nibble(&mut sky_light, flat_index, nbt::get_nibble(sky, i)); + } + } + if let Some(ref bl) = section.block_light { + if bl.len() > i { + nbt::set_nibble(&mut block_light, flat_index, nbt::get_nibble(bl, i)); + } + } + } + } + } + + (blocks, data, sky_light, block_light) +} + +pub fn convert_chunk( + root_tag: &nbt::NbtCompound, + new_chunk_x: i32, + new_chunk_z: i32, + preserve_dynamic: bool, +) -> Vec { + let level = build_legacy_chunk_level(root_tag, new_chunk_x, new_chunk_z, preserve_dynamic); + payload::encode_legacy_nbt(&level) +} + +pub fn convert_chunk_for_save( + root_tag: &nbt::NbtCompound, + new_chunk_x: i32, + new_chunk_z: i32, + preserve_dynamic: bool, +) -> Vec { + let level = build_legacy_chunk_level(root_tag, new_chunk_x, new_chunk_z, preserve_dynamic); + payload::encode_compressed_storage(&level) +} + +fn build_legacy_chunk_level( + root_tag: &nbt::NbtCompound, + new_chunk_x: i32, + new_chunk_z: i32, + preserve_dynamic: bool, +) -> nbt::NbtCompound { + let format_info = inspect_chunk(root_tag); + let source_level = root_tag + .compound("Level") + .unwrap_or(root_tag); + let is_modern = format_info.uses_modern_content_schema(); + let (mut blocks, mut data, mut sky_light, mut block_light) = + if format_info.is_section_based() { + flatten_anvil_sections(source_level, &format_info, &mut None) + } else { + let b = get_byte_array_or_default(source_level, "Blocks", CHUNK_BLOCKS); + let d = get_byte_array_or_default(source_level, "Data", CHUNK_NIBBLES); + let s = get_byte_array_or_default(source_level, "SkyLight", CHUNK_NIBBLES); + let bl = get_byte_array_or_default(source_level, "BlockLight", CHUNK_NIBBLES); + (b, d, s, bl) + }; + + let height_map = get_byte_array_or_default(source_level, "HeightMap", HEIGHTMAP_SIZE); + let mut biomes = get_byte_array_or_default(source_level, "Biomes", BIOMES_SIZE); + if is_modern { + biomes.fill(1); + } + + block_mapping::remap_blocks(&mut blocks, &mut data); + let last_update = source_level.long("LastUpdate").unwrap_or(0); + let inhabited_time = source_level.long("InhabitedTime").unwrap_or(0); + const TERRAIN_POPULATED_FLAGS: i16 = 0; + sky_light.fill(0); + block_light.fill(0); + let mut level = nbt::NbtCompound::new("Level"); + level.insert("xPos", nbt::NbtValue::Int(new_chunk_x)); + level.insert("zPos", nbt::NbtValue::Int(new_chunk_z)); + level.insert("LastUpdate", nbt::NbtValue::Long(last_update)); + level.insert("InhabitedTime", nbt::NbtValue::Long(inhabited_time)); + level.insert("Blocks", nbt::NbtValue::ByteArray(blocks)); + level.insert("Data", nbt::NbtValue::ByteArray(data)); + level.insert("SkyLight", nbt::NbtValue::ByteArray(sky_light)); + level.insert("BlockLight", nbt::NbtValue::ByteArray(block_light)); + level.insert("HeightMap", nbt::NbtValue::ByteArray(height_map)); + level.insert("TerrainPopulatedFlags", nbt::NbtValue::Short(TERRAIN_POPULATED_FLAGS)); + level.insert("Biomes", nbt::NbtValue::ByteArray(biomes)); + let source_chunk_x = source_level.int("xPos").unwrap_or(new_chunk_x); + let source_chunk_z = source_level.int("zPos").unwrap_or(new_chunk_z); + let block_offset_x = (source_chunk_x - new_chunk_x) * 16; + let block_offset_z = (source_chunk_z - new_chunk_z) * 16; + if preserve_dynamic && !is_modern { + let mut entities: Vec = source_level + .list("Entities") + .map(|l| l.to_vec()) + .unwrap_or_default(); + remap_entity_positions(&mut entities, block_offset_x, block_offset_z); + level.insert("Entities", nbt::NbtValue::List(entities)); + } else { + level.insert("Entities", nbt::NbtValue::List(Vec::new())); + } + + let mut tile_entities = if preserve_dynamic && !is_modern { + build_compatible_tile_entities(source_level, false) + } else { + build_safe_sign_tile_entities(source_level, is_modern) + }; + remove_unsupported_tile_entities(&mut tile_entities); + remap_tile_entity_positions(&mut tile_entities, block_offset_x, block_offset_z); + level.insert("TileEntities", nbt::NbtValue::List(tile_entities)); + if preserve_dynamic && !is_modern { + if let Some(ticks) = source_level.list("TileTicks") { + let remapped = remap_tile_tick_positions(ticks, block_offset_x, block_offset_z); + level.insert("TileTicks", nbt::NbtValue::List(remapped)); + } else if let Some(ticks) = source_level.list("block_ticks") { + let remapped = remap_tile_tick_positions(ticks, block_offset_x, block_offset_z); + level.insert("TileTicks", nbt::NbtValue::List(remapped)); + } + } + + level +} + +fn build_compatible_tile_entities( + source_level: &nbt::NbtCompound, + is_modern: bool, +) -> Vec { + if !is_modern { + let tiles = source_level.list("TileEntities"); + if let Some(t) = tiles { + return t.to_vec(); + } + let tiles2 = source_level.list("block_entities"); + if let Some(t) = tiles2 { + return t.to_vec(); + } + return Vec::new(); + } + build_safe_sign_tile_entities(source_level, true) +} + +fn build_safe_sign_tile_entities( + source_level: &nbt::NbtCompound, + is_modern: bool, +) -> Vec { + let mut result = Vec::new(); + let source = if is_modern { + source_level + .list("block_entities") + .or_else(|| source_level.list("TileEntities")) + } else { + source_level + .list("TileEntities") + .or_else(|| source_level.list("block_entities")) + }; + + let source = match source { + Some(s) => s, + None => return result, + }; + + for tag in source { + if let nbt::NbtValue::Compound(block_entity) = tag { + let id = block_entity.string("id").unwrap_or(""); + let is_sign = if is_modern { + is_modern_sign_tile_entity(id) + } else { + id == "Sign" || id == "minecraft:sign" + }; + if !is_sign { + continue; + } + let sign = build_safe_legacy_sign_tile_entity(block_entity, is_modern); + result.push(nbt::NbtValue::Compound(sign)); + } + } + + result +} + +fn is_modern_sign_tile_entity(id: &str) -> bool { + let stripped = id.strip_prefix("minecraft:").unwrap_or(id); + matches!( + stripped, + "sign" + | "hanging_sign" + | "oak_sign" + | "spruce_sign" + | "birch_sign" + | "jungle_sign" + | "acacia_sign" + | "dark_oak_sign" + | "mangrove_sign" + | "cherry_sign" + | "bamboo_sign" + | "crimson_sign" + | "warped_sign" + | "oak_hanging_sign" + | "spruce_hanging_sign" + | "birch_hanging_sign" + | "jungle_hanging_sign" + | "acacia_hanging_sign" + | "dark_oak_hanging_sign" + | "mangrove_hanging_sign" + | "cherry_hanging_sign" + | "bamboo_hanging_sign" + | "crimson_hanging_sign" + | "warped_hanging_sign" + ) +} + +fn read_tile_entity_coord(compound: &nbt::NbtCompound, name: &str) -> i32 { + compound.int(name) + .map(|v| v as i32) + .or_else(|| compound.short(name).map(|v| v as i32)) + .or_else(|| compound.byte(name).map(|v| v as i32)) + .unwrap_or(0) +} + +fn extract_modern_sign_lines(block_entity: &nbt::NbtCompound) -> [String; 4] { + let mut lines: [String; 4] = Default::default(); + if let Some(front_text) = block_entity.compound("front_text") { + if let Some(messages) = front_text.list("messages") { + for i in 0..4.min(messages.len()) { + if let nbt::NbtValue::String(s) = &messages[i] { + lines[i] = sanitize_legacy_sign_line(&simplify_sign_text(s)); + } + } + return lines; + } + } + + for i in 0..4 { + let key = format!("Text{}", i + 1); + let value = block_entity.string(&key).unwrap_or(""); + lines[i] = sanitize_legacy_sign_line(&simplify_sign_text(value)); + } + + lines +} + +fn extract_legacy_sign_lines(block_entity: &nbt::NbtCompound) -> [String; 4] { + let mut lines: [String; 4] = Default::default(); + for i in 0..4 { + let key = format!("Text{}", i + 1); + let value = block_entity.string(&key).unwrap_or(""); + lines[i] = sanitize_legacy_sign_line(&simplify_sign_text(value)); + } + lines +} + +fn sanitize_legacy_sign_line(raw: &str) -> String { + let mut builder = String::new(); + for ch in raw.chars() { + if ch == '\r' || ch == '\n' || ch == '\t' || ch == '\0' { + continue; + } + if ch.is_control() { + continue; + } + builder.push(ch); + if builder.len() >= 15 { + break; + } + } + builder +} + +fn simplify_sign_text(raw: &str) -> String { + let raw = raw.trim(); + if !(raw.starts_with('{') || raw.starts_with('[') || raw.starts_with('"')) { + return raw.to_string(); + } + + if let Ok(doc) = serde_json::from_str::(raw) { + let mut builder = String::new(); + append_json_text(&doc, &mut builder); + return builder; + } + + raw.trim_matches('"').to_string() +} + +fn append_json_text(element: &serde_json::Value, builder: &mut String) { + match element { + serde_json::Value::String(s) => builder.push_str(s), + serde_json::Value::Array(arr) => { + for item in arr { + append_json_text(item, builder); + } + } + serde_json::Value::Object(obj) => { + if let Some(serde_json::Value::String(text)) = obj.get("text") { + builder.push_str(text); + } + if let Some(extra) = obj.get("extra") { + append_json_text(extra, builder); + } + } + _ => {} + } +} + +fn build_safe_legacy_sign_tile_entity( + block_entity: &nbt::NbtCompound, + is_modern: bool, +) -> nbt::NbtCompound { + let x = read_tile_entity_coord(block_entity, "x"); + let y = read_tile_entity_coord(block_entity, "y"); + let z = read_tile_entity_coord(block_entity, "z"); + let lines = if is_modern { + extract_modern_sign_lines(block_entity) + } else { + extract_legacy_sign_lines(block_entity) + }; + + let mut sign = nbt::NbtCompound::new(""); + sign.insert("id", nbt::NbtValue::String("Sign".to_string())); + sign.insert("x", nbt::NbtValue::Int(x)); + sign.insert("y", nbt::NbtValue::Int(y)); + sign.insert("z", nbt::NbtValue::Int(z)); + sign.insert("Text1", nbt::NbtValue::String(lines[0].clone())); + sign.insert("Text2", nbt::NbtValue::String(lines[1].clone())); + sign.insert("Text3", nbt::NbtValue::String(lines[2].clone())); + sign.insert("Text4", nbt::NbtValue::String(lines[3].clone())); + sign +} + +fn remap_tile_tick_positions( + ticks: &[nbt::NbtValue], + block_offset_x: i32, + block_offset_z: i32, +) -> Vec { + if block_offset_x == 0 && block_offset_z == 0 { + return ticks.to_vec(); + } + let mut result = Vec::new(); + for tag in ticks { + if let nbt::NbtValue::Compound(mut tick) = tag.clone() { + if let Some(nbt::NbtValue::Int(x)) = tick.get_mut("x") { + *x -= block_offset_x; + } + if let Some(nbt::NbtValue::Int(z)) = tick.get_mut("z") { + *z -= block_offset_z; + } + result.push(nbt::NbtValue::Compound(tick)); + } + } + result +} + +fn remap_entity_positions(entities: &mut Vec, block_offset_x: i32, block_offset_z: i32) { + if block_offset_x == 0 && block_offset_z == 0 { + return; + } + for tag in entities.iter_mut() { + if let nbt::NbtValue::Compound(entity) = tag { + if let Some(nbt::NbtValue::List(pos)) = entity.get_mut("Pos") { + if pos.len() >= 3 { + if let nbt::NbtValue::Double(x) = &mut pos[0] { + *x -= block_offset_x as f64; + } + if let nbt::NbtValue::Double(z) = &mut pos[2] { + *z -= block_offset_z as f64; + } + } + } + if let Some(nbt::NbtValue::Compound(riding)) = entity.get_mut("Riding") { + let mut inner = vec![nbt::NbtValue::Compound(riding.clone())]; + remap_entity_positions(&mut inner, block_offset_x, block_offset_z); + if let Some(nbt::NbtValue::Compound(updated)) = inner.into_iter().next() { + *riding = updated; + } + } + } + } +} + +fn remove_unsupported_tile_entities(tile_entities: &mut Vec) { + tile_entities.retain(|tag| { + if let nbt::NbtValue::Compound(te) = tag { + let id = te.string("id").unwrap_or(""); + return id != "Control" && id != "minecraft:command_block" && id != "CommandBlock"; + } + false + }); +} + +fn remap_tile_entity_positions(tile_entities: &mut Vec, block_offset_x: i32, block_offset_z: i32) { + if block_offset_x == 0 && block_offset_z == 0 { + return; + } + for tag in tile_entities.iter_mut() { + if let nbt::NbtValue::Compound(te) = tag { + if let Some(nbt::NbtValue::Int(x)) = te.get_mut("x") { + *x -= block_offset_x; + } + if let Some(nbt::NbtValue::Int(z)) = te.get_mut("z") { + *z -= block_offset_z; + } + } + } +} + +pub fn build_modern_anvil_level( + legacy_level: &nbt::NbtCompound, + chunk_x: i32, + chunk_z: i32, +) -> nbt::NbtCompound { + let default_blocks = vec![0u8; 32768]; + let default_data = vec![0u8; 16384]; + let old_blocks_ref = legacy_level + .byte_array("Blocks") + .unwrap_or(&default_blocks); + let old_data_ref = legacy_level + .byte_array("Data") + .unwrap_or(&default_data); + + let mut section_tags: Vec = Vec::new(); + for section_y in 0..8 { + let mut palette_list: Vec = Vec::new(); + let mut palette_dict: HashMap = HashMap::new(); + let mut indices = [0i32; 4096]; + let mut has_non_air = false; + for y in 0..16 { + for z in 0..16 { + for x in 0..16 { + let global_y = section_y * 16 + y; + let legacy_flat_index = ((x * 16) + z) * 128 + global_y; + if legacy_flat_index >= old_blocks_ref.len() { + continue; + } + let block_id = old_blocks_ref[legacy_flat_index]; + let meta = nbt::get_nibble(old_data_ref, legacy_flat_index); + let modern_state = get_contextual_block_state(block_id, meta, x, global_y, z, old_blocks_ref, old_data_ref); + if modern_state != "minecraft:air" { + has_non_air = true; + } + + let idx = *palette_dict + .entry(modern_state.clone()) + .or_insert_with(|| { + let idx = palette_list.len(); + palette_list.push(modern_state); + idx + }); + + indices[y * 256 + z * 16 + x] = idx as i32; + } + } + } + + if !has_non_air && section_y > 0 { + continue; + } + + let mut section_tag = nbt::NbtCompound::new(""); + section_tag.insert("Y", nbt::NbtValue::Byte(section_y as i8)); + let mut block_states_tag = nbt::NbtCompound::new("block_states"); + let mut nbt_palette: Vec = Vec::new(); + for state in &palette_list { + let mut p_tag = nbt::NbtCompound::new(""); + if let Some(bracket_idx) = state.find('[') { + let name = &state[..bracket_idx]; + let props_str = &state[bracket_idx + 1..state.len() - 1]; + p_tag.insert("Name", nbt::NbtValue::String(name.to_string())); + let mut props_compound = nbt::NbtCompound::new("Properties"); + for prop in props_str.split(',') { + let kv: Vec<&str> = prop.splitn(2, '=').collect(); + if kv.len() == 2 { + props_compound + .insert(kv[0], nbt::NbtValue::String(kv[1].to_string())); + } + } + p_tag.insert("Properties", nbt::NbtValue::Compound(props_compound)); + } else { + p_tag.insert("Name", nbt::NbtValue::String(state.to_string())); + } + nbt_palette.push(nbt::NbtValue::Compound(p_tag)); + } + block_states_tag.insert("palette", nbt::NbtValue::List(nbt_palette)); + if palette_list.len() > 1 { + let bits_per_block = (64.0 / (palette_list.len() as f64).log2().ceil()).floor().max(4.0) as usize; + let values_per_long = 64 / bits_per_block; + let long_count = ((4096.0 / values_per_long as f64).ceil()) as usize; + let mut data_array = vec![0i64; long_count]; + let mut current_long: u64 = 0; + let mut blocks_in_current_long = 0; + let mut long_index = 0; + for i in 0..4096 { + let val = indices[i] as u64; + current_long |= val << (blocks_in_current_long * bits_per_block); + blocks_in_current_long += 1; + if blocks_in_current_long >= values_per_long { + data_array[long_index] = current_long as i64; + long_index += 1; + current_long = 0; + blocks_in_current_long = 0; + } + } + + if blocks_in_current_long > 0 { + data_array[long_index] = current_long as i64; + } + + block_states_tag.insert("data", nbt::NbtValue::LongArray(data_array)); + } + + section_tag.insert("block_states", nbt::NbtValue::Compound(block_states_tag)); + let mut biomes_tag = nbt::NbtCompound::new("biomes"); + let biome_palette = nbt::NbtValue::List(vec![nbt::NbtValue::String( + "minecraft:plains".to_string(), + )]); + biomes_tag.insert("palette", biome_palette); + section_tag.insert("biomes", nbt::NbtValue::Compound(biomes_tag)); + section_tags.push(nbt::NbtValue::Compound(section_tag)); + } + + let mut root = nbt::NbtCompound::new(""); + root.insert("xPos", nbt::NbtValue::Int(chunk_x)); + root.insert("zPos", nbt::NbtValue::Int(chunk_z)); + root.insert("yPos", nbt::NbtValue::Int(0)); + root.insert("Status", nbt::NbtValue::String("full".to_string())); + let mut block_entities: Vec = Vec::new(); + if let Some(old_te_list) = legacy_level.list("TileEntities") { + for old_te in old_te_list { + if let nbt::NbtValue::Compound(old_te_comp) = old_te { + let mut new_te = old_te_comp.clone(); + let id = new_te.string("id").unwrap_or("").to_string(); + let new_id = match id.as_str() { + "Chest" => Some("minecraft:chest"), + "Furnace" => Some("minecraft:furnace"), + "BrewingStand" => Some("minecraft:brewing_stand"), + "EnchantTable" => Some("minecraft:enchanting_table"), + "Trap" => Some("minecraft:dispenser"), + "MobSpawner" => Some("minecraft:mob_spawner"), + "Control" => Some("minecraft:command_block"), + "Beacon" => Some("minecraft:beacon"), + "Skull" => Some("minecraft:skull"), + "Sign" => Some("minecraft:sign"), + "Cauldron" => Some("minecraft:cauldron"), + "Dropper" => Some("minecraft:dropper"), + "Hopper" => Some("minecraft:hopper"), + "Comparator" => Some("minecraft:comparator"), + "RecordPlayer" => Some("minecraft:jukebox"), + "Banner" => Some("minecraft:banner"), + _ if id.starts_with("minecraft:") => Some(id.as_str()), + _ => None, + }; + + if let Some(nid) = new_id { + new_te.insert("id", nbt::NbtValue::String(nid.to_string())); + block_entities.push(nbt::NbtValue::Compound(new_te)); + } + } + } + } + + for i in 0..old_blocks_ref.len().min(32768) { + if old_blocks_ref[i] == 26 { + let y = i % 128; + let z = (i / 128) % 16; + let x = i / 2048; + let mut bed_te = nbt::NbtCompound::new(""); + bed_te.insert("id", nbt::NbtValue::String("minecraft:bed".to_string())); + bed_te.insert("color", nbt::NbtValue::Int(14)); + bed_te.insert("x", nbt::NbtValue::Int(chunk_x * 16 + x as i32)); + bed_te.insert("y", nbt::NbtValue::Int(y as i32)); + bed_te.insert("z", nbt::NbtValue::Int(chunk_z * 16 + z as i32)); + block_entities.push(nbt::NbtValue::Compound(bed_te)); + } + } + + root.insert( + "block_entities", + nbt::NbtValue::List(block_entities), + ); + root.insert("sections", nbt::NbtValue::List(section_tags)); + root +} + +fn get_contextual_block_state( + block_id: u8, + meta: u8, + x: usize, + global_y: usize, + z: usize, + old_blocks: &[u8], + old_data: &[u8], +) -> String { + let modern_state = get_modern_block_name(block_id, meta); + let get_neighbor = |nx: i32, ny: i32, nz: i32| -> (u8, u8) { + if nx < 0 || nx > 15 || ny < 0 || ny > 127 || nz < 0 || nz > 15 { + return (0, 0); + } + let idx = ((nx as usize * 16) + nz as usize) * 128 + ny as usize; + if idx >= old_blocks.len() { + return (0, 0); + } + let n_id = old_blocks[idx]; + let n_meta = nbt::get_nibble(old_data, idx); + (n_id, n_meta) + }; + + let bracket_index = modern_state.find('['); + let name = if let Some(bi) = bracket_index { + &modern_state[..bi] + } else { + &modern_state + }; + let mut properties: HashMap = HashMap::new(); + if let Some(bi) = bracket_index { + let props_str = &modern_state[bi + 1..modern_state.len() - 1]; + for prop in props_str.split(',') { + let kv: Vec<&str> = prop.splitn(2, '=').collect(); + if kv.len() == 2 { + properties.insert(kv[0].to_string(), kv[1].to_string()); + } + } + } + + let mut properties_changed = false; + if block_id == 64 || block_id == 71 || (block_id >= 193 && block_id <= 197) { + let is_top = (meta & 8) == 8; + properties.insert("half".to_string(), if is_top { "upper" } else { "lower" }.to_string()); + if is_top { + properties.insert( + "hinge".to_string(), + if (meta & 1) == 1 { "right" } else { "left" }.to_string(), + ); + let (bottom_id, bottom_meta) = get_neighbor(x as i32, global_y as i32 - 1, z as i32); + if bottom_id == block_id { + let facing_meta = bottom_meta & 3; + properties.insert( + "facing".to_string(), + match facing_meta { + 0 => "east", + 1 => "south", + 2 => "west", + _ => "north", + } + .to_string(), + ); + properties.insert( + "open".to_string(), + if (bottom_meta & 4) == 4 { "true" } else { "false" }.to_string(), + ); + } else { + properties.insert("facing".to_string(), "east".to_string()); + properties.insert("open".to_string(), "false".to_string()); + } + } else { + let facing_meta = meta & 3; + properties.insert( + "facing".to_string(), + match facing_meta { + 0 => "east", + 1 => "south", + 2 => "west", + _ => "north", + } + .to_string(), + ); + properties.insert( + "open".to_string(), + if (meta & 4) == 4 { "true" } else { "false" }.to_string(), + ); + let (top_id, top_meta) = get_neighbor(x as i32, global_y as i32 + 1, z as i32); + properties.insert( + "hinge".to_string(), + if top_id == block_id { + if (top_meta & 1) == 1 { "right" } else { "left" } + } else { + "left" + } + .to_string(), + ); + } + properties_changed = true; + } else if block_id == 63 || block_id == 68 { + if block_id == 68 { + properties.insert( + "facing".to_string(), + match meta { + 2 => "north", + 3 => "south", + 4 => "west", + _ => "east", + } + .to_string(), + ); + } else { + properties.insert("rotation".to_string(), meta.to_string()); + } + properties.remove("waterlogged"); + properties_changed = true; + } else if block_id == 50 || block_id == 75 || block_id == 76 { + if meta >= 1 && meta <= 4 { + properties.insert( + "facing".to_string(), + match meta { + 1 => "east", + 2 => "west", + 3 => "south", + _ => "north", + } + .to_string(), + ); + } + if block_id == 76 { + properties.insert("lit".to_string(), "true".to_string()); + } else if block_id == 75 { + properties.insert("lit".to_string(), "false".to_string()); + } + properties_changed = true; + } else if block_id == 54 || block_id == 146 { + let facing = match meta { + 2 => "north", + 3 => "south", + 4 => "west", + 5 => "east", + _ => "south", + }; + properties.insert("type".to_string(), "single".to_string()); + properties.insert("facing".to_string(), facing.to_string()); + let dirs = [(-1i32, 0i32), (1, 0), (0, -1), (0, 1)]; + for (dx, dz) in dirs { + let (nid, _) = get_neighbor(x as i32 + dx, global_y as i32, z as i32 + dz); + if nid == block_id { + let is_left = match facing { + "north" => dx == -1, + "south" => dx == 1, + "west" => dz == 1, + "east" => dz == -1, + _ => false, + }; + properties.insert( + "type".to_string(), + if is_left { "right" } else { "left" }.to_string(), + ); + break; + } + } + properties_changed = true; + } else if is_stair_legacy(block_id) { + let facing = get_stair_facing(meta); + let half = if (meta & STAIR_UPSIDE_DOWN_BIT) != 0 { + "top" + } else { + "bottom" + }; + properties.insert("facing".to_string(), facing.to_string()); + properties.insert("half".to_string(), half.to_string()); + properties.insert("shape".to_string(), "straight".to_string()); + let (front_dx, front_dz) = match facing { + "north" => (0i32, 1i32), + "south" => (0, -1), + "west" => (1, 0), + "east" => (-1, 0), + _ => (0, 0), + }; + let (back_dx, back_dz) = match facing { + "north" => (0i32, -1i32), + "south" => (0, 1), + "west" => (-1, 0), + "east" => (1, 0), + _ => (0, 0), + }; + + let back_neighbor = get_neighbor(x as i32 + back_dx, global_y as i32, z as i32 + back_dz); + if is_stair_legacy(back_neighbor.0) { + let back_facing = get_stair_facing(back_neighbor.1); + let back_half = if (back_neighbor.1 & STAIR_UPSIDE_DOWN_BIT) != 0 { + "top" + } else { + "bottom" + }; + if back_half == half { + if let Some(side) = get_relative_side(facing, &back_facing) { + properties.insert("shape".to_string(), format!("outer_{}", side)); + } + } + } else { + let front_neighbor = get_neighbor(x as i32 + front_dx, global_y as i32, z as i32 + front_dz); + if is_stair_legacy(front_neighbor.0) { + let front_facing = get_stair_facing(front_neighbor.1); + let front_half = if (front_neighbor.1 & STAIR_UPSIDE_DOWN_BIT) != 0 { + "top" + } else { + "bottom" + }; + if front_half == half { + if let Some(side) = get_relative_side(facing, &front_facing) { + properties.insert("shape".to_string(), format!("inner_{}", side)); + } + } + } + } + properties_changed = true; + } else if is_fence_legacy(block_id) { + let (n_id, _) = get_neighbor(x as i32, global_y as i32, z as i32 - 1); + let (e_id, _) = get_neighbor(x as i32 + 1, global_y as i32, z as i32); + let (s_id, _) = get_neighbor(x as i32, global_y as i32, z as i32 + 1); + let (w_id, _) = get_neighbor(x as i32 - 1, global_y as i32, z as i32); + properties.insert( + "north".to_string(), + (is_fence_legacy(n_id) || is_fence_gate_legacy(n_id) || is_likely_solid_attachment(n_id)) + .to_string(), + ); + properties.insert( + "east".to_string(), + (is_fence_legacy(e_id) || is_fence_gate_legacy(e_id) || is_likely_solid_attachment(e_id)) + .to_string(), + ); + properties.insert( + "south".to_string(), + (is_fence_legacy(s_id) || is_fence_gate_legacy(s_id) || is_likely_solid_attachment(s_id)) + .to_string(), + ); + properties.insert( + "west".to_string(), + (is_fence_legacy(w_id) || is_fence_gate_legacy(w_id) || is_likely_solid_attachment(w_id)) + .to_string(), + ); + properties_changed = true; + } else if is_pane_legacy(block_id) { + let (n_id, _) = get_neighbor(x as i32, global_y as i32, z as i32 - 1); + let (e_id, _) = get_neighbor(x as i32 + 1, global_y as i32, z as i32); + let (s_id, _) = get_neighbor(x as i32, global_y as i32, z as i32 + 1); + let (w_id, _) = get_neighbor(x as i32 - 1, global_y as i32, z as i32); + properties.insert( + "north".to_string(), + (is_pane_legacy(n_id) || is_glass_legacy(n_id) || is_likely_solid_attachment(n_id)) + .to_string(), + ); + properties.insert( + "east".to_string(), + (is_pane_legacy(e_id) || is_glass_legacy(e_id) || is_likely_solid_attachment(e_id)) + .to_string(), + ); + properties.insert( + "south".to_string(), + (is_pane_legacy(s_id) || is_glass_legacy(s_id) || is_likely_solid_attachment(s_id)) + .to_string(), + ); + properties.insert( + "west".to_string(), + (is_pane_legacy(w_id) || is_glass_legacy(w_id) || is_likely_solid_attachment(w_id)) + .to_string(), + ); + properties_changed = true; + } + + if properties_changed || name == "minecraft:red_bed" { + if !properties.is_empty() { + let parts: Vec = properties.iter().map(|(k, v)| format!("{}={}", k, v)).collect(); + return format!("{}[{}]", name, parts.join(",")); + } + return name.to_string(); + } + + modern_state +} + +fn get_modern_block_name(block_id: u8, meta: u8) -> String { + let name = get_contextual_modern_name(block_id, meta); + if let Some(props) = get_contextual_properties(block_id, meta) { + format!("{}[{}]", name, props) + } else { + name + } +} + +fn get_contextual_modern_name(block_id: u8, meta: u8) -> String { + match block_id { + 0 => "minecraft:air".into(), + 1 => "minecraft:stone".into(), + 2 => "minecraft:grass_block".into(), + 3 => "minecraft:dirt".into(), + 4 => "minecraft:cobblestone".into(), + 5 => match meta { + 1 => "minecraft:spruce_planks", + 2 => "minecraft:birch_planks", + 3 => "minecraft:jungle_planks", + _ => "minecraft:oak_planks", + } + .into(), + 6 => match meta { + 1 => "minecraft:spruce_sapling", + 2 => "minecraft:birch_sapling", + 3 => "minecraft:jungle_sapling", + _ => "minecraft:oak_sapling", + } + .into(), + 7 => "minecraft:bedrock".into(), + 8 | 9 => "minecraft:water".into(), + 10 | 11 => "minecraft:lava".into(), + 12 => "minecraft:sand".into(), + 13 => "minecraft:gravel".into(), + 14 => "minecraft:gold_ore".into(), + 15 => "minecraft:iron_ore".into(), + 16 => "minecraft:coal_ore".into(), + 17 => match meta & 3 { + 1 => "minecraft:spruce_log", + 2 => "minecraft:birch_log", + 3 => "minecraft:jungle_log", + _ => "minecraft:oak_log", + } + .into(), + 18 => match meta & 3 { + 1 => "minecraft:spruce_leaves", + 2 => "minecraft:birch_leaves", + 3 => "minecraft:jungle_leaves", + _ => "minecraft:oak_leaves", + } + .into(), + 19 => "minecraft:sponge".into(), + 20 => "minecraft:glass".into(), + 21 => "minecraft:lapis_ore".into(), + 22 => "minecraft:lapis_block".into(), + 23 => "minecraft:dispenser".into(), + 24 => match meta { + 1 => "minecraft:chiseled_sandstone", + 2 => "minecraft:smooth_sandstone", + _ => "minecraft:sandstone", + } + .into(), + 25 => "minecraft:note_block".into(), + 26 => "minecraft:red_bed".into(), + 27 => "minecraft:powered_rail".into(), + 28 => "minecraft:detector_rail".into(), + 29 => "minecraft:sticky_piston".into(), + 30 => "minecraft:cobweb".into(), + 31 => "minecraft:short_grass".into(), + 32 => "minecraft:dead_bush".into(), + 33 => "minecraft:piston".into(), + 35 => match meta { + 1 => "minecraft:orange_wool", + 2 => "minecraft:magenta_wool", + 3 => "minecraft:light_blue_wool", + 4 => "minecraft:yellow_wool", + 5 => "minecraft:lime_wool", + 6 => "minecraft:pink_wool", + 7 => "minecraft:gray_wool", + 8 => "minecraft:light_gray_wool", + 9 => "minecraft:cyan_wool", + 10 => "minecraft:purple_wool", + 11 => "minecraft:blue_wool", + 12 => "minecraft:brown_wool", + 13 => "minecraft:green_wool", + 14 => "minecraft:red_wool", + 15 => "minecraft:black_wool", + _ => "minecraft:white_wool", + } + .into(), + 37 => "minecraft:dandelion".into(), + 38 => "minecraft:poppy".into(), + 39 => "minecraft:brown_mushroom".into(), + 40 => "minecraft:red_mushroom".into(), + 41 => "minecraft:gold_block".into(), + 42 => "minecraft:iron_block".into(), + 44 => { + let variant = meta & 7; + let name = match variant { + 1 => "minecraft:sandstone_slab", + 2 => "minecraft:stone_slab", + 3 => "minecraft:cobblestone_slab", + 4 => "minecraft:brick_slab", + 5 => "minecraft:stone_brick_slab", + 6 => "minecraft:nether_brick_slab", + 7 => "minecraft:quartz_slab", + _ => "minecraft:stone_slab", + }; + return name.to_string(); + } + 45 => "minecraft:bricks".into(), + 46 => "minecraft:tnt".into(), + 47 => "minecraft:bookshelf".into(), + 48 => "minecraft:mossy_cobblestone".into(), + 49 => "minecraft:obsidian".into(), + 50 => "minecraft:torch".into(), + 51 => "minecraft:fire".into(), + 52 => "minecraft:mob_spawner".into(), + 53 => "minecraft:oak_stairs".into(), + 54 => "minecraft:chest".into(), + 55 => "minecraft:redstone_wire".into(), + 56 => "minecraft:diamond_ore".into(), + 57 => "minecraft:diamond_block".into(), + 58 => "minecraft:crafting_table".into(), + 59 => "minecraft:wheat".into(), + 60 => "minecraft:farmland".into(), + 61 => "minecraft:furnace".into(), + 63 => "minecraft:oak_sign".into(), + 64 => "minecraft:oak_door".into(), + 65 => "minecraft:ladder".into(), + 66 => "minecraft:rail".into(), + 67 => "minecraft:cobblestone_stairs".into(), + 68 => "minecraft:oak_wall_sign".into(), + 69 => "minecraft:lever".into(), + 70 => "minecraft:stone_pressure_plate".into(), + 71 => "minecraft:iron_door".into(), + 72 => "minecraft:oak_pressure_plate".into(), + 73 => "minecraft:redstone_ore".into(), + 75 => "minecraft:redstone_wall_torch".into(), + 76 => "minecraft:redstone_torch".into(), + 77 => "minecraft:stone_button".into(), + 78 => "minecraft:snow".into(), + 79 => "minecraft:ice".into(), + 80 => "minecraft:snow_block".into(), + 81 => "minecraft:cactus".into(), + 82 => "minecraft:clay".into(), + 83 => "minecraft:sugar_cane".into(), + 84 => "minecraft:jukebox".into(), + 85 => "minecraft:oak_fence".into(), + 86 => "minecraft:carved_pumpkin".into(), + 87 => "minecraft:netherrack".into(), + 88 => "minecraft:soul_sand".into(), + 89 => "minecraft:glowstone".into(), + 90 => "minecraft:nether_portal".into(), + 91 => "minecraft:jack_o_lantern".into(), + 92 => "minecraft:cake".into(), + 93 => "minecraft:unpowered_repeater".into(), + 94 => "minecraft:powered_repeater".into(), + 96 => "minecraft:trapdoor".into(), + 98 => "minecraft:stone_bricks".into(), + 101 => "minecraft:iron_bars".into(), + 102 => "minecraft:glass_pane".into(), + 103 => "minecraft:melon".into(), + 104 => "minecraft:pumpkin_stem".into(), + 105 => "minecraft:melon_stem".into(), + 106 => "minecraft:vine".into(), + 107 => "minecraft:oak_fence_gate".into(), + 108 => "minecraft:brick_stairs".into(), + 109 => "minecraft:stone_brick_stairs".into(), + 110 => "minecraft:mycelium".into(), + 111 => "minecraft:lily_pad".into(), + 112 => "minecraft:nether_bricks".into(), + 113 => "minecraft:nether_brick_fence".into(), + 114 => "minecraft:nether_brick_stairs".into(), + 115 => "minecraft:nether_wart".into(), + 116 => "minecraft:enchanting_table".into(), + 117 => "minecraft:brewing_stand".into(), + 118 => "minecraft:cauldron".into(), + 121 => "minecraft:end_stone".into(), + 122 => "minecraft:dragon_egg".into(), + 123 => "minecraft:redstone_lamp".into(), + 124 => "minecraft:redstone_lamp".into(), + 125 => "minecraft:double_wooden_slab".into(), + 126 => "minecraft:wooden_slab".into(), + 127 => "minecraft:cocoa".into(), + 128 => "minecraft:sandstone_stairs".into(), + 129 => "minecraft:emerald_ore".into(), + 130 => "minecraft:ender_chest".into(), + 131 => "minecraft:tripwire_hook".into(), + 132 => "minecraft:tripwire".into(), + 133 => "minecraft:emerald_block".into(), + 134 => "minecraft:spruce_stairs".into(), + 135 => "minecraft:birch_stairs".into(), + 136 => "minecraft:jungle_stairs".into(), + 138 => "minecraft:beacon".into(), + 139 => "minecraft:cobblestone_wall".into(), + 140 => "minecraft:flower_pot".into(), + 141 => "minecraft:carrots".into(), + 142 => "minecraft:potatoes".into(), + 143 => "minecraft:wooden_button".into(), + 144 => "minecraft:skull".into(), + 145 => "minecraft:anvil".into(), + 146 => "minecraft:trapped_chest".into(), + 147 => "minecraft:light_weighted_pressure_plate".into(), + 148 => "minecraft:heavy_weighted_pressure_plate".into(), + 149 => "minecraft:unpowered_comparator".into(), + 150 => "minecraft:powered_comparator".into(), + 151 => "minecraft:daylight_detector".into(), + 152 => "minecraft:redstone_block".into(), + 153 => "minecraft:nether_quartz_ore".into(), + 154 => "minecraft:hopper".into(), + 155 => match meta { + 1 => "minecraft:chiseled_quartz_block", + 2 => "minecraft:quartz_pillar", + 3 => "minecraft:quartz_pillar", + _ => "minecraft:quartz_block", + } + .into(), + 156 => "minecraft:quartz_stairs".into(), + 157 => "minecraft:activator_rail".into(), + 158 => "minecraft:dropper".into(), + 159 => match meta { + 0 => "minecraft:white_stained_hardened_clay", + 1 => "minecraft:orange_stained_hardened_clay", + 2 => "minecraft:magenta_stained_hardened_clay", + 3 => "minecraft:light_blue_stained_hardened_clay", + 4 => "minecraft:yellow_stained_hardened_clay", + 5 => "minecraft:lime_stained_hardened_clay", + 6 => "minecraft:pink_stained_hardened_clay", + 7 => "minecraft:gray_stained_hardened_clay", + 8 => "minecraft:light_gray_stained_hardened_clay", + 9 => "minecraft:cyan_stained_hardened_clay", + 10 => "minecraft:purple_stained_hardened_clay", + 11 => "minecraft:blue_stained_hardened_clay", + 12 => "minecraft:brown_stained_hardened_clay", + 13 => "minecraft:green_stained_hardened_clay", + 14 => "minecraft:red_stained_hardened_clay", + 15 => "minecraft:black_stained_hardened_clay", + _ => "minecraft:white_stained_hardened_clay", + } + .into(), + 160 => match meta { + 0 => "minecraft:white_stained_glass_pane", + 1 => "minecraft:orange_stained_glass_pane", + 2 => "minecraft:magenta_stained_glass_pane", + 3 => "minecraft:light_blue_stained_glass_pane", + 4 => "minecraft:yellow_stained_glass_pane", + 5 => "minecraft:lime_stained_glass_pane", + 6 => "minecraft:pink_stained_glass_pane", + 7 => "minecraft:gray_stained_glass_pane", + 8 => "minecraft:light_gray_stained_glass_pane", + 9 => "minecraft:cyan_stained_glass_pane", + 10 => "minecraft:purple_stained_glass_pane", + 11 => "minecraft:blue_stained_glass_pane", + 12 => "minecraft:brown_stained_glass_pane", + 13 => "minecraft:green_stained_glass_pane", + 14 => "minecraft:red_stained_glass_pane", + 15 => "minecraft:black_stained_glass_pane", + _ => "minecraft:white_stained_glass_pane", + } + .into(), + 161 => match meta & 3 { + 1 => "minecraft:acacia_leaves", + _ => "minecraft:dark_oak_leaves", + } + .into(), + 162 => match meta & 3 { + 1 => "minecraft:acacia_log", + _ => "minecraft:dark_oak_log", + } + .into(), + 163 => "minecraft:acacia_stairs".into(), + 164 => "minecraft:dark_oak_stairs".into(), + 170 => "minecraft:hay_block".into(), + 171 => match meta { + 0 => "minecraft:white_carpet", + 1 => "minecraft:orange_carpet", + 2 => "minecraft:magenta_carpet", + 3 => "minecraft:light_blue_carpet", + 4 => "minecraft:yellow_carpet", + 5 => "minecraft:lime_carpet", + 6 => "minecraft:pink_carpet", + 7 => "minecraft:gray_carpet", + 8 => "minecraft:light_gray_carpet", + 9 => "minecraft:cyan_carpet", + 10 => "minecraft:purple_carpet", + 11 => "minecraft:blue_carpet", + 12 => "minecraft:brown_carpet", + 13 => "minecraft:green_carpet", + 14 => "minecraft:red_carpet", + 15 => "minecraft:black_carpet", + _ => "minecraft:white_carpet", + } + .into(), + 172 => match meta { + 0 => "minecraft:white_concrete", + 1 => "minecraft:orange_concrete", + 2 => "minecraft:magenta_concrete", + 3 => "minecraft:light_blue_concrete", + 4 => "minecraft:yellow_concrete", + 5 => "minecraft:lime_concrete", + 6 => "minecraft:pink_concrete", + 7 => "minecraft:gray_concrete", + 8 => "minecraft:light_gray_concrete", + 9 => "minecraft:cyan_concrete", + 10 => "minecraft:purple_concrete", + 11 => "minecraft:blue_concrete", + 12 => "minecraft:brown_concrete", + 13 => "minecraft:green_concrete", + 14 => "minecraft:red_concrete", + 15 => "minecraft:black_concrete", + _ => "minecraft:white_concrete", + } + .into(), + 173 => "minecraft:coal_block".into(), + 95 => match meta { + 0 => "minecraft:white_stained_glass", + 1 => "minecraft:orange_stained_glass", + 2 => "minecraft:magenta_stained_glass", + 3 => "minecraft:light_blue_stained_glass", + 4 => "minecraft:yellow_stained_glass", + 5 => "minecraft:lime_stained_glass", + 6 => "minecraft:pink_stained_glass", + 7 => "minecraft:gray_stained_glass", + 8 => "minecraft:light_gray_stained_glass", + 9 => "minecraft:cyan_stained_glass", + 10 => "minecraft:purple_stained_glass", + 11 => "minecraft:blue_stained_glass", + 12 => "minecraft:brown_stained_glass", + 13 => "minecraft:green_stained_glass", + 14 => "minecraft:red_stained_glass", + 15 => "minecraft:black_stained_glass", + _ => "minecraft:white_stained_glass", + } + .into(), + _ => "minecraft:air".into(), + } +} + +fn get_contextual_properties(block_id: u8, meta: u8) -> Option { + match block_id { + 63 => Some(format!("rotation={}", meta & 0x0F)), + 68 => Some(format!( + "facing={}", + match meta { + 2 => "north", + 3 => "south", + 4 => "west", + _ => "east", + } + )), + 50 | 75 | 76 => { + if meta >= 1 && meta <= 4 { + Some(format!( + "facing={}", + match meta { + 1 => "east", + 2 => "west", + 3 => "south", + _ => "north", + } + )) + } else { + None + } + } + _ => None, + } +} + +fn is_stair_legacy(id: u8) -> bool { + matches!( + id, + 53 | 67 | 108 | 109 | 114 | 128 | 134 | 135 | 136 | 156 | 163 | 164 | 180 + ) +} + +fn get_stair_facing(meta: u8) -> &'static str { + match meta & 0x3 { + 0 => "east", + 1 => "west", + 2 => "south", + 3 => "north", + _ => "north", + } +} + +fn get_relative_side(current_facing: &str, neighbor_facing: &str) -> Option<&'static str> { + match current_facing { + "north" => { + if neighbor_facing == "west" { + return Some("left"); + } + if neighbor_facing == "east" { + return Some("right"); + } + } + "south" => { + if neighbor_facing == "east" { + return Some("left"); + } + if neighbor_facing == "west" { + return Some("right"); + } + } + "west" => { + if neighbor_facing == "south" { + return Some("left"); + } + if neighbor_facing == "north" { + return Some("right"); + } + } + "east" => { + if neighbor_facing == "north" { + return Some("left"); + } + if neighbor_facing == "south" { + return Some("right"); + } + } + _ => {} + } + None +} + +fn is_fence_legacy(id: u8) -> bool { + matches!(id, 85 | 113 | 188 | 189 | 190 | 191 | 192) +} + +fn is_fence_gate_legacy(id: u8) -> bool { + matches!(id, 107 | 183 | 184 | 185 | 186 | 187) +} + +fn is_pane_legacy(id: u8) -> bool { + matches!(id, 101 | 102 | 160) +} + +fn is_glass_legacy(id: u8) -> bool { + matches!(id, 20 | 95) +} + +fn is_likely_solid_attachment(id: u8) -> bool { + if id == 0 { + return false; + } + if id >= 8 && id <= 11 { + return false; + } + !matches!( + id, + 6 | 27 | 28 | 30 | 31 | 32 | 37 | 38 | 39 | 40 | 50 | 51 | 55 | 59 | 63 | 65 | 66 + | 68 | 69 | 70 | 71 | 72 | 75 | 76 | 77 | 78 | 83 | 90 | 92 | 93 | 94 | 96 | 101 + | 102 | 104 | 105 | 106 | 107 | 111 | 115 | 119 | 127 | 131 | 132 | 141 | 142 | 143 + | 147 | 148 | 149 | 150 | 157 | 160 | 171 | 175 + ) +} diff --git a/src-tauri/src/commands/java2lce/level_dat.rs b/src-tauri/src/commands/java2lce/level_dat.rs new file mode 100644 index 0000000..dba4f04 --- /dev/null +++ b/src-tauri/src/commands/java2lce/level_dat.rs @@ -0,0 +1,251 @@ +use super::nbt::{self, NbtCompound, NbtValue}; +pub struct SpawnPoint { + pub x: i32, + pub y: i32, + pub z: i32, +} + +pub fn read_spawn(root: &NbtCompound) -> SpawnPoint { + let data = match root.compound("Data") { + Some(d) => d, + None => { + return SpawnPoint { x: 0, y: 64, z: 0 }; + } + }; + + if let (Some(sx), Some(sz)) = (data.int("SpawnX"), data.int("SpawnZ")) { + return SpawnPoint { + x: sx, + y: data.int("SpawnY").unwrap_or(64), + z: sz, + }; + } + + if let Some(spawn_comp) = data.compound("spawn") { + if let Some(pos) = spawn_comp.int_array("pos") { + if pos.len() >= 3 { + return SpawnPoint { + x: pos[0], + y: pos[1], + z: pos[2], + }; + } + } + } + + SpawnPoint { x: 0, y: 64, z: 0 } +} + +pub fn convert_java_to_lce( + java_root: &NbtCompound, + spawn_chunk_x: i32, + spawn_chunk_z: i32, + xz_size: i32, + flat_world: bool, + override_spawn_y: Option, +) -> Vec { + let java_data = java_root + .compound("Data") + .expect("Java level.dat missing 'Data' compound tag"); + + let data_version = java_data + .int("DataVersion") + .or_else(|| java_root.int("DataVersion")) + .unwrap_or(0); + let is_modern_world = data_version >= 1519; + let hell_scale: i32 = 3; + let spawn = read_spawn(java_root); + let spawn_x = spawn.x; + let spawn_y = if let Some(oy) = override_spawn_y { + oy.clamp(1, 127) + } else if is_modern_world { + 64 + } else { + spawn.y.clamp(1, 127) + }; + let spawn_z = spawn.z; + let new_spawn_x = spawn_x - (spawn_chunk_x * 16); + let new_spawn_z = spawn_z - (spawn_chunk_z * 16); + let safe_generator_name = if flat_world { + "flat".to_string() + } else if is_modern_world { + "default".to_string() + } else { + get_string(java_data, "generatorName", "default") + }; + let safe_generator_version = if flat_world { + 0 + } else if is_modern_world { + 1 + } else { + get_int(java_data, "generatorVersion") + }; + let safe_generator_options = if flat_world { + get_string(java_data, "generatorOptions", "2;7,2x3,2;1;") + } else if is_modern_world { + "".to_string() + } else { + get_string(java_data, "generatorOptions", "") + }; + + let mut lce_data = NbtCompound::new("Data"); + lce_data.insert("RandomSeed", NbtValue::Long(get_long(java_data, "RandomSeed"))); + lce_data.insert("generatorName", NbtValue::String(safe_generator_name)); + lce_data.insert("generatorVersion", NbtValue::Int(safe_generator_version)); + lce_data.insert("generatorOptions", NbtValue::String(safe_generator_options)); + lce_data.insert("GameType", NbtValue::Int(get_int(java_data, "GameType"))); + lce_data.insert("MapFeatures", NbtValue::Byte(get_bool(java_data, "MapFeatures", true))); + lce_data.insert("SpawnX", NbtValue::Int(new_spawn_x)); + lce_data.insert("SpawnY", NbtValue::Int(spawn_y)); + lce_data.insert("SpawnZ", NbtValue::Int(new_spawn_z)); + lce_data.insert("Time", NbtValue::Long(get_long(java_data, "Time"))); + lce_data.insert("DayTime", NbtValue::Long(get_long(java_data, "DayTime"))); + lce_data.insert("SizeOnDisk", NbtValue::Long(0)); + lce_data.insert("LastPlayed", NbtValue::Long(unix_timestamp_millis())); + lce_data.insert( + "LevelName", + NbtValue::String(get_string(java_data, "LevelName", "Converted World")), + ); + lce_data.insert("version", NbtValue::Int(19133)); + lce_data.insert("rainTime", NbtValue::Int(get_int(java_data, "rainTime"))); + lce_data.insert("raining", NbtValue::Byte(get_bool(java_data, "raining", false))); + lce_data.insert("thunderTime", NbtValue::Int(get_int(java_data, "thunderTime"))); + lce_data.insert( + "thundering", + NbtValue::Byte(get_bool(java_data, "thundering", false)), + ); + lce_data.insert( + "hardcore", + NbtValue::Byte(get_bool(java_data, "hardcore", false)), + ); + lce_data.insert( + "allowCommands", + NbtValue::Byte(get_bool(java_data, "allowCommands", false)), + ); + lce_data.insert( + "initialized", + NbtValue::Byte(get_bool(java_data, "initialized", true)), + ); + + lce_data.insert("newSeaLevel", NbtValue::Byte(1)); + lce_data.insert( + "hasBeenInCreative", + NbtValue::Byte(get_bool(java_data, "hasBeenInCreative", false)), + ); + lce_data.insert( + "spawnBonusChest", + NbtValue::Byte(get_bool(java_data, "spawnBonusChest", false)), + ); + + lce_data.insert("hasStronghold", NbtValue::Byte(0)); + lce_data.insert("StrongholdX", NbtValue::Int(0)); + lce_data.insert("StrongholdY", NbtValue::Int(0)); + lce_data.insert("StrongholdZ", NbtValue::Int(0)); + lce_data.insert("hasStrongholdEndPortal", NbtValue::Byte(0)); + lce_data.insert("StrongholdEndPortalX", NbtValue::Int(0)); + lce_data.insert("StrongholdEndPortalZ", NbtValue::Int(0)); + lce_data.insert("XZSize", NbtValue::Int(xz_size)); + lce_data.insert("HellScale", NbtValue::Int(hell_scale)); + if !is_modern_world { + if let Some(game_rules) = java_data.get("GameRules") { + lce_data.insert("GameRules", game_rules.clone()); + } + } + + let mut root = NbtCompound::new(""); + root.insert("Data", NbtValue::Compound(lce_data)); + nbt::write_nbt(&root) +} + +pub fn convert_lce_to_java( + lce_root: &NbtCompound, + override_spawn_x: Option, + override_spawn_y: Option, + override_spawn_z: Option, + embedded_player: Option<&NbtCompound>, +) -> Vec { + let lce_data = lce_root.compound("Data").unwrap_or(lce_root); + let mut java_data = lce_data.clone(); + let lce_only_fields = [ + "newSeaLevel", + "hasBeenInCreative", + "spawnBonusChest", + "XZSize", + "xzSize", + "HellScale", + "hellScale", + "hasStronghold", + "StrongholdX", + "StrongholdY", + "StrongholdZ", + "hasStrongholdEndPortal", + "StrongholdEndPortalX", + "StrongholdEndPortalZ", + "xStronghold", + "yStronghold", + "zStronghold", + "hasStrongholdEP", + "xStrongholdEP", + "zStrongholdEP", + ]; + + for field in lce_only_fields { + java_data.remove(field); + } + + if let Some(sx) = override_spawn_x { + java_data.insert("SpawnX", NbtValue::Int(sx)); + } + if let Some(sy) = override_spawn_y { + java_data.insert("SpawnY", NbtValue::Int(sy)); + } + if let Some(sz) = override_spawn_z { + java_data.insert("SpawnZ", NbtValue::Int(sz)); + } + + java_data.insert("version", NbtValue::Int(19133)); + java_data.insert("DataVersion", NbtValue::Int(1343)); + let mut version_compound = NbtCompound::new("Version"); + version_compound.insert("Id", NbtValue::Int(1343)); + version_compound.insert("Name", NbtValue::String("1.12.2".to_string())); + version_compound.insert("Snapshot", NbtValue::Byte(0)); + java_data.insert("Version", NbtValue::Compound(version_compound)); + if let Some(player) = embedded_player { + let mut player_tag = player.clone(); + player_tag.name = "Player".to_string(); + java_data.insert("Player", NbtValue::Compound(player_tag)); + } + + java_data.insert("LastPlayed", NbtValue::Long(unix_timestamp_millis())); + let mut java_root = NbtCompound::new(""); + let mut data_tag = NbtCompound::new("Data"); + for (name, value) in &java_data.tags { + data_tag.insert(name, value.clone()); + } + java_root.insert("Data", NbtValue::Compound(data_tag)); + + nbt::write_gzip_nbt(&java_root) +} + +fn get_long(compound: &NbtCompound, name: &str) -> i64 { + compound.long(name).unwrap_or(0) +} + +fn get_int(compound: &NbtCompound, name: &str) -> i32 { + compound.int(name).unwrap_or(0) +} + +fn get_string(compound: &NbtCompound, name: &str, default: &str) -> String { + compound.string(name).unwrap_or(default).to_string() +} + +fn get_bool(compound: &NbtCompound, name: &str, default: bool) -> i8 { + compound.byte(name).unwrap_or(if default { 1 } else { 0 }) +} + +fn unix_timestamp_millis() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64 +} diff --git a/src-tauri/src/commands/java2lce/mod.rs b/src-tauri/src/commands/java2lce/mod.rs new file mode 100644 index 0000000..3531912 --- /dev/null +++ b/src-tauri/src/commands/java2lce/mod.rs @@ -0,0 +1,235 @@ +pub mod block_mapping; +pub mod chunk; +pub mod payload; +pub mod level_dat; +pub mod nbt; +pub mod region; +use std::fs; +use std::path::Path; +use tauri::AppHandle; +use std::collections::HashMap; +use region::{JavaWorldReader, JavaRegionFileWriter, LceRegionFile, SaveDataContainer}; +use chunk::{convert_chunk_for_save, build_modern_anvil_level}; +use level_dat::{read_spawn, convert_java_to_lce, convert_lce_to_java}; +const REGION_SECT_COUNT: usize = 1024; +#[tauri::command] +#[allow(non_snake_case)] +pub async fn java_to_lce( + _app: AppHandle, + java_world_path: String, + output_ms_path: String, +) -> Result { + let reader = JavaWorldReader::new(&java_world_path); + let java_root = reader.read_level_dat().map_err(|e| format!("Failed to read level.dat: {}", e))?; + let spawn = read_spawn(&java_root); + let spawn_chunk_x = spawn.x >> 4; + let spawn_chunk_z = spawn.z >> 4; + let region_files = reader.get_region_files(""); + let total_regions = region_files.len(); + let mut container = SaveDataContainer::new(0, 0); + let level_dat_bytes = convert_java_to_lce( + &java_root, + spawn_chunk_x, + spawn_chunk_z, + 864, + false, + None, + ); + let ld_idx = container.create_file("level.dat"); + container.write_to_file(ld_idx, &level_dat_bytes); + let mut java_reader = JavaWorldReader::new(&java_world_path); + let mut chunks_written: usize = 0; + let mut errors: Vec = Vec::new(); + for (ri, &(_, _, ref region_path)) in region_files.iter().enumerate() { + let parts: Vec<&str> = region_path.split('.').collect(); + let region_x: i32 = if parts.len() >= 2 { + parts[parts.len() - 3].parse().unwrap_or(0) + } else { + 0 + }; + let region_z: i32 = if parts.len() >= 2 { + parts[parts.len() - 2].parse().unwrap_or(0) + } else { + 0 + }; + + let region_filename = format!("r.{}.{}.mcr", region_x, region_z); + let mut lce_region = LceRegionFile::new(®ion_filename); + let mut chunk_count = 0; + for local_z in 0..32 { + for local_x in 0..32 { + if !java_reader.has_chunk(region_path, local_x, local_z) { + continue; + } + + match java_reader.read_chunk_nbt(region_path, local_x, local_z) { + Ok(Some(root)) => { + let chunk_x = region_x * 32 + local_x; + let chunk_z = region_z * 32 + local_z; + let payload = convert_chunk_for_save(&root, chunk_x, chunk_z, false); + lce_region.write_chunk(local_x, local_z, payload); + chunk_count += 1; + } + Ok(None) => {} + Err(e) => { + errors.push(format!("r.{}/{}: {}", local_x, local_z, e)); + } + } + } + } + + if chunk_count > 0 { + lce_region.write_to_container(&mut container); + chunks_written += chunk_count; + } + + if ri % 10 == 0 { + eprintln!("java2lce: processed region {}/{}", ri + 1, total_regions); + } + } + + container.save(&output_ms_path).map_err(|e| format!("Failed to save output: {}", e))?; + let mut msg = format!( + "Conversion complete!\nChunks: {}\nRegions: {}", + chunks_written, total_regions + ); + if !errors.is_empty() { + msg.push_str(&format!("\nErrors: {}", errors.len())); + for err in errors.iter().take(5) { + msg.push_str(&format!("\n {}", err)); + } + if errors.len() > 5 { + msg.push_str(&format!("\n ... and {} more", errors.len() - 5)); + } + } + eprintln!("{}",msg); + Ok(msg) +} + +#[tauri::command] +#[allow(non_snake_case)] +pub async fn lce_to_java( + _app: AppHandle, + input_ms_path: String, + java_world_output: String, +) -> Result { + let ms_data = fs::read(&input_ms_path).map_err(|e| format!("Failed to read saveData.ms: {}", e))?; + let decompressed = region::decompress_zlib(&ms_data[8..]).map_err(|e| format!("Failed to decompress save data: {}", e))?; + let footer_offset = u32::from_le_bytes([ + decompressed[0], decompressed[1], decompressed[2], decompressed[3], + ]) as usize; + let entry_count = u32::from_le_bytes([ + decompressed[4], decompressed[5], decompressed[6], decompressed[7], + ]) as usize; + let mut files: Vec<(String, Vec)> = Vec::new(); + for i in 0..entry_count { + let base = footer_offset + i * 144; + if base + 144 > decompressed.len() { + break; + } + + let name_bytes = &decompressed[base..base + 128]; + let name: String = name_bytes + .chunks_exact(2) + .map(|c| u16::from_le_bytes([c[0], c[1]])) + .take_while(|&c| c != 0) + .filter_map(|c| char::from_u32(c as u32)) + .collect(); + + let length = u32::from_le_bytes([ + decompressed[base + 128], + decompressed[base + 129], + decompressed[base + 130], + decompressed[base + 131], + ]) as usize; + let start_offset = u32::from_le_bytes([ + decompressed[base + 132], + decompressed[base + 133], + decompressed[base + 134], + decompressed[base + 135], + ]) as usize; + if !name.is_empty() && length > 0 && start_offset + length <= decompressed.len() { + let data = decompressed[start_offset..start_offset + length].to_vec(); + files.push((name, data)); + } + } + + let out_dir = Path::new(&java_world_output); + fs::create_dir_all(out_dir).map_err(|e| format!("Failed to create output directory: {}", e))?; + fs::create_dir_all(out_dir.join("region")).map_err(|e| format!("Failed to create region directory: {}", e))?; + let mut chunks_written: usize = 0; + let mut errors: Vec = Vec::new(); + let mut region_writers: HashMap = HashMap::new(); + for (name, data) in &files { + if name == "level.dat" { + let lce_root = nbt::read_nbt(data).map_err(|e| format!("Failed to parse level.dat: {}", e))?; + let java_level_bytes = convert_lce_to_java(&lce_root, None, None, None, None); + fs::write(out_dir.join("level.dat"), &java_level_bytes).map_err(|e| format!("Failed to write level.dat: {}", e))?; + continue; + } + + if name.ends_with(".mcr") || name.ends_with(".mca") { + let mut reader = region::JavaRegionReader::open_from_bytes(data); + let parts: Vec<&str> = name.split('.').collect(); + let region_x: i32 = if parts.len() >= 4 { parts[1].parse().unwrap_or(0) } else { 0 }; + let region_z: i32 = if parts.len() >= 4 { parts[2].parse().unwrap_or(0) } else { 0 }; + for local_z in 0..32 { + for local_x in 0..32 { + let index = (local_x & 31) + (local_z & 31) * 32; + if reader.offsets[index as usize] == 0 { + continue; + } + + match reader.read_chunk(local_x, local_z) { + Ok(Some(lce_chunk_data)) => { + if let Some(legacy_nbt) = payload::try_decode_to_legacy_nbt(&lce_chunk_data) { + let root = nbt::read_nbt(&legacy_nbt).unwrap_or_default(); + let source_level = root.compound("Level").unwrap_or(&root); + let cx = region_x * 32 + local_x; + let cz = region_z * 32 + local_z; + let modern_root = build_modern_anvil_level(source_level, cx, cz); + let modern_nbt = nbt::write_nbt(&modern_root); + let out_region_x = cx >> 5; + let out_region_z = cz >> 5; + let out_local_x = cx & 31; + let out_local_z = cz & 31; + let section_region = format!("r.{}.{}.mca", out_region_x, out_region_z); + let region_path = out_dir.join("region").join(§ion_region); + let region_path_str = region_path.to_string_lossy().to_string(); + let writer = region_writers.entry(section_region.clone()).or_insert_with(|| { + JavaRegionFileWriter::load_from_file(®ion_path_str) + }); + writer.write_chunk(out_local_x, out_local_z, &modern_nbt); + chunks_written += 1; + } + } + Ok(None) => {} + Err(e) => { + errors.push(format!("{}/{}: {}", name, local_x * 32 + local_z, e)); + } + } + } + } + continue; + } + } + + for (region_name, writer) in &mut region_writers { + if let Err(e) = writer.save() { + errors.push(format!("Failed to save region {}: {}", region_name, e)); + } + } + + let mut msg = format!( + "Conversion complete!\nChunks: {}\nFiles: {}", + chunks_written, files.len() + ); + if !errors.is_empty() { + msg.push_str(&format!("\nErrors: {}", errors.len())); + for err in errors.iter().take(5) { + msg.push_str(&format!("\n {}", err)); + } + } + eprintln!("{}", msg); + Ok(msg) +} diff --git a/src-tauri/src/commands/java2lce/nbt.rs b/src-tauri/src/commands/java2lce/nbt.rs new file mode 100644 index 0000000..bc0324f --- /dev/null +++ b/src-tauri/src/commands/java2lce/nbt.rs @@ -0,0 +1,496 @@ +use std::io::{Read, Write}; +use flate2::read::GzDecoder; +use flate2::write::GzEncoder; +use flate2::Compression; +#[derive(Debug, Clone, PartialEq)] +pub enum NbtValue { + Byte(i8), + Short(i16), + Int(i32), + Long(i64), + Float(f32), + Double(f64), + ByteArray(Vec), + String(String), + List(Vec), + Compound(NbtCompound), + IntArray(Vec), + LongArray(Vec), +} + +#[derive(Debug, Clone, Default, PartialEq)] +pub struct NbtCompound { + pub name: String, + pub tags: Vec<(String, NbtValue)>, +} + +impl NbtCompound { + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + tags: Vec::new(), + } + } + + pub fn insert(&mut self, name: &str, value: NbtValue) { + self.tags.retain(|(n, _)| n != name); + self.tags.push((name.to_string(), value)); + } + + pub fn get(&self, name: &str) -> Option<&NbtValue> { + self.tags.iter().find(|(n, _)| n == name).map(|(_, v)| v) + } + + pub fn get_mut(&mut self, name: &str) -> Option<&mut NbtValue> { + self.tags.iter_mut().find(|(n, _)| n == name).map(|(_, v)| v) + } + + pub fn contains(&self, name: &str) -> bool { + self.tags.iter().any(|(n, _)| n == name) + } + + pub fn remove(&mut self, name: &str) { + self.tags.retain(|(n, _)| n != name); + } + + pub fn compound(&self, name: &str) -> Option<&NbtCompound> { + self.get(name).and_then(|v| match v { + NbtValue::Compound(c) => Some(c), + _ => None, + }) + } + + pub fn compound_mut(&mut self, name: &str) -> Option<&mut NbtCompound> { + self.get_mut(name).and_then(|v| match v { + NbtValue::Compound(c) => Some(c), + _ => None, + }) + } + + pub fn byte(&self, name: &str) -> Option { + self.get(name).and_then(|v| match v { + NbtValue::Byte(b) => Some(*b), + _ => None, + }) + } + + pub fn short(&self, name: &str) -> Option { + self.get(name).and_then(|v| match v { + NbtValue::Short(s) => Some(*s), + _ => None, + }) + } + + pub fn int(&self, name: &str) -> Option { + self.get(name).and_then(|v| match v { + NbtValue::Int(i) => Some(*i), + _ => None, + }) + } + + pub fn long(&self, name: &str) -> Option { + self.get(name).and_then(|v| match v { + NbtValue::Long(l) => Some(*l), + _ => None, + }) + } + + pub fn float(&self, name: &str) -> Option { + self.get(name).and_then(|v| match v { + NbtValue::Float(f) => Some(*f), + _ => None, + }) + } + + pub fn double(&self, name: &str) -> Option { + self.get(name).and_then(|v| match v { + NbtValue::Double(d) => Some(*d), + _ => None, + }) + } + + pub fn string(&self, name: &str) -> Option<&str> { + self.get(name).and_then(|v| match v { + NbtValue::String(s) => Some(s.as_str()), + _ => None, + }) + } + + pub fn byte_array(&self, name: &str) -> Option<&[u8]> { + self.get(name).and_then(|v| match v { + NbtValue::ByteArray(b) => Some(b.as_slice()), + _ => None, + }) + } + + pub fn int_array(&self, name: &str) -> Option<&[i32]> { + self.get(name).and_then(|v| match v { + NbtValue::IntArray(a) => Some(a.as_slice()), + _ => None, + }) + } + + pub fn long_array(&self, name: &str) -> Option<&[i64]> { + self.get(name).and_then(|v| match v { + NbtValue::LongArray(a) => Some(a.as_slice()), + _ => None, + }) + } + + pub fn list(&self, name: &str) -> Option<&[NbtValue]> { + self.get(name).and_then(|v| match v { + NbtValue::List(l) => Some(l.as_slice()), + _ => None, + }) + } + + pub fn list_compounds(&self, name: &str) -> Vec<&NbtCompound> { + self.list(name) + .unwrap_or(&[]) + .iter() + .filter_map(|v| match v { + NbtValue::Compound(c) => Some(c), + _ => None, + }) + .collect() + } +} + +fn tag_type_id(v: &NbtValue) -> u8 { + match v { + NbtValue::Byte(_) => 1, + NbtValue::Short(_) => 2, + NbtValue::Int(_) => 3, + NbtValue::Long(_) => 4, + NbtValue::Float(_) => 5, + NbtValue::Double(_) => 6, + NbtValue::ByteArray(_) => 7, + NbtValue::String(_) => 8, + NbtValue::List(_) => 9, + NbtValue::Compound(_) => 10, + NbtValue::IntArray(_) => 11, + NbtValue::LongArray(_) => 12, + } +} + +fn read_be_i16(data: &[u8], off: usize) -> i16 { + i16::from_be_bytes([data[off], data[off + 1]]) +} + +fn read_be_i32(data: &[u8], off: usize) -> i32 { + i32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) +} + +fn read_be_i64(data: &[u8], off: usize) -> i64 { + i64::from_be_bytes([ + data[off], + data[off + 1], + data[off + 2], + data[off + 3], + data[off + 4], + data[off + 5], + data[off + 6], + data[off + 7], + ]) +} + +fn read_be_f32(data: &[u8], off: usize) -> f32 { + f32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) +} + +fn read_be_f64(data: &[u8], off: usize) -> f64 { + f64::from_be_bytes([ + data[off], + data[off + 1], + data[off + 2], + data[off + 3], + data[off + 4], + data[off + 5], + data[off + 6], + data[off + 7], + ]) +} + +fn read_string(data: &[u8], off: usize) -> Result<(String, usize), String> { + let len = read_be_i16(data, off) as usize; + let start = off + 2; + if start + len > data.len() { + return Err("string extends past end of data".into()); + } + let s = String::from_utf8_lossy(&data[start..start + len]).to_string(); + Ok((s, start + len)) +} + +pub fn read_nbt(data: &[u8]) -> Result { + if data.is_empty() { + return Err("empty NBT data".into()); + } + let mut pos = 0; + let tag_type = data[pos]; + pos += 1; + if tag_type != 10 { + return Err(format!("root tag is not compound (type={})", tag_type)); + } + let (name, new_pos) = read_string(data, pos)?; + pos = new_pos; + let (compound, _new_pos) = read_compound_payload(data, pos)?; + let mut result = compound; + result.name = name; + Ok(result) +} + +fn read_compound_payload(data: &[u8], mut pos: usize) -> Result<(NbtCompound, usize), String> { + let mut compound = NbtCompound::default(); + loop { + if pos >= data.len() { + return Err("unexpected end in compound".into()); + } + let tag_type = data[pos]; + pos += 1; + if tag_type == 0 { + break; + } + let (name, new_pos) = read_string(data, pos)?; + pos = new_pos; + let (value, new_pos) = read_value(data, pos, tag_type)?; + pos = new_pos; + compound.tags.push((name, value)); + } + Ok((compound, pos)) +} + +fn read_value(data: &[u8], pos: usize, tag_type: u8) -> Result<(NbtValue, usize), String> { + match tag_type { + 1 => Ok((NbtValue::Byte(data[pos] as i8), pos + 1)), + 2 => Ok((NbtValue::Short(read_be_i16(data, pos)), pos + 2)), + 3 => Ok((NbtValue::Int(read_be_i32(data, pos)), pos + 4)), + 4 => Ok((NbtValue::Long(read_be_i64(data, pos)), pos + 8)), + 5 => Ok((NbtValue::Float(read_be_f32(data, pos)), pos + 4)), + 6 => Ok((NbtValue::Double(read_be_f64(data, pos)), pos + 8)), + 7 => { + let len = read_be_i32(data, pos) as usize; + let start = pos + 4; + if start + len > data.len() { + return Err("byte array extends past end".into()); + } + Ok((NbtValue::ByteArray(data[start..start + len].to_vec()), start + len)) + } + 8 => { + let (s, new_pos) = read_string(data, pos)?; + Ok((NbtValue::String(s), new_pos)) + } + 9 => { + let elem_type = data[pos]; + let count = read_be_i32(data, pos + 1) as usize; + let mut list_pos = pos + 5; + let mut list = Vec::with_capacity(count); + for _ in 0..count { + let (val, new_pos) = read_value(data, list_pos, elem_type)?; + list_pos = new_pos; + list.push(val); + } + Ok((NbtValue::List(list), list_pos)) + } + 10 => { + let (compound, new_pos) = read_compound_payload(data, pos)?; + Ok((NbtValue::Compound(compound), new_pos)) + } + 11 => { + let len = read_be_i32(data, pos) as usize; + let start = pos + 4; + let mut arr = Vec::with_capacity(len); + for i in 0..len { + let off = start + i * 4; + if off + 4 > data.len() { + return Err("int array extends past end".into()); + } + arr.push(read_be_i32(data, off)); + } + Ok((NbtValue::IntArray(arr), start + len * 4)) + } + 12 => { + let len = read_be_i32(data, pos) as usize; + let start = pos + 4; + let mut arr = Vec::with_capacity(len); + for i in 0..len { + let off = start + i * 8; + if off + 8 > data.len() { + return Err("long array extends past end".into()); + } + arr.push(read_be_i64(data, off)); + } + Ok((NbtValue::LongArray(arr), start + len * 8)) + } + _ => Err(format!("unknown NBT tag type: {}", tag_type)), + } +} + +pub fn write_nbt(compound: &NbtCompound) -> Vec { + let mut out = Vec::new(); + out.push(10); + write_string(&mut out, &compound.name); + write_compound_payload(&mut out, compound); + out.push(0); + out +} + +fn write_string(out: &mut Vec, s: &str) { + let bytes = s.as_bytes(); + let len = (bytes.len() as i16).to_be_bytes(); + out.extend_from_slice(&len); + out.extend_from_slice(bytes); +} + +fn write_compound_payload(out: &mut Vec, compound: &NbtCompound) { + for (name, value) in &compound.tags { + out.push(tag_type_id(value)); + write_string(out, name); + write_value(out, value); + } +} + +fn write_value(out: &mut Vec, value: &NbtValue) { + match value { + NbtValue::Byte(b) => out.push(*b as u8), + NbtValue::Short(s) => out.extend_from_slice(&s.to_be_bytes()), + NbtValue::Int(i) => out.extend_from_slice(&i.to_be_bytes()), + NbtValue::Long(l) => out.extend_from_slice(&l.to_be_bytes()), + NbtValue::Float(f) => out.extend_from_slice(&f.to_be_bytes()), + NbtValue::Double(d) => out.extend_from_slice(&d.to_be_bytes()), + NbtValue::ByteArray(arr) => { + out.extend_from_slice(&(arr.len() as i32).to_be_bytes()); + out.extend_from_slice(arr); + } + NbtValue::String(s) => write_string(out, s), + NbtValue::List(list) => { + let elem_type = list.first().map(|v| tag_type_id(v)).unwrap_or(0); + out.push(elem_type); + out.extend_from_slice(&(list.len() as i32).to_be_bytes()); + for item in list { + write_value(out, item); + } + } + NbtValue::Compound(c) => { + write_compound_payload(out, c); + out.push(0); + } + NbtValue::IntArray(arr) => { + out.extend_from_slice(&(arr.len() as i32).to_be_bytes()); + for i in arr { + out.extend_from_slice(&i.to_be_bytes()); + } + } + NbtValue::LongArray(arr) => { + out.extend_from_slice(&(arr.len() as i32).to_be_bytes()); + for l in arr { + out.extend_from_slice(&l.to_be_bytes()); + } + } + } +} + +pub fn read_gzip_nbt(data: &[u8]) -> Result { + let mut decoder = GzDecoder::new(data); + let mut buf = Vec::new(); + decoder + .read_to_end(&mut buf) + .map_err(|e| format!("gzip decompress failed: {}", e))?; + read_nbt(&buf) +} + +pub fn write_gzip_nbt(compound: &NbtCompound) -> Vec { + let raw = write_nbt(compound); + let mut encoder = GzEncoder::new(Vec::new(), Compression::best()); + encoder.write_all(&raw).unwrap(); + encoder.finish().unwrap() +} + +pub fn read_zlib_nbt(data: &[u8]) -> Result { + use flate2::read::ZlibDecoder; + let mut decoder = ZlibDecoder::new(data); + let mut buf = Vec::new(); + decoder + .read_to_end(&mut buf) + .map_err(|e| format!("zlib decompress failed: {}", e))?; + read_nbt(&buf) +} + +pub fn write_zlib_nbt(compound: &NbtCompound) -> Vec { + use flate2::write::ZlibEncoder; + let raw = write_nbt(compound); + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best()); + encoder.write_all(&raw).unwrap(); + encoder.finish().unwrap() +} + +pub fn get_byte_array_or(compound: &NbtCompound, name: &str, default_len: usize) -> Vec { + compound + .byte_array(name) + .map(|b| { + let mut v = b.to_vec(); + if v.len() < default_len { + v.resize(default_len, 0); + } + v + }) + .unwrap_or_else(|| vec![0u8; default_len]) +} + +pub fn get_int_array_or(compound: &NbtCompound, name: &str, default_len: usize) -> Vec { + compound + .int_array(name) + .map(|a| { + let mut v = a.to_vec(); + if v.len() < default_len { + v.resize(default_len, 0); + } + v + }) + .unwrap_or_else(|| vec![0i32; default_len]) +} + +pub fn get_long_array_or(compound: &NbtCompound, name: &str, default_len: usize) -> Vec { + compound + .long_array(name) + .map(|a| { + let mut v = a.to_vec(); + if v.len() < default_len { + v.resize(default_len, 0); + } + v + }) + .unwrap_or_else(|| vec![0i64; default_len]) +} + +pub fn get_nibble(data: &[u8], index: usize) -> u8 { + let byte_index = index >> 1; + if byte_index >= data.len() { + return 0; + } + let b = data[byte_index]; + if index & 1 == 0 { + b & 0x0F + } else { + (b >> 4) & 0x0F + } +} + +pub fn set_nibble(data: &mut [u8], index: usize, value: u8) { + let byte_index = index >> 1; + if byte_index >= data.len() { + return; + } + let val = value & 0x0F; + if index & 1 == 0 { + data[byte_index] = (data[byte_index] & 0xF0) | val; + } else { + data[byte_index] = (data[byte_index] & 0x0F) | (val << 4); + } +} + +pub fn clone_or_empty_list(compound: &NbtCompound, name: &str) -> Vec { + compound + .list(name) + .map(|l| l.to_vec()) + .unwrap_or_default() +} diff --git a/src-tauri/src/commands/java2lce/payload.rs b/src-tauri/src/commands/java2lce/payload.rs new file mode 100644 index 0000000..5238467 --- /dev/null +++ b/src-tauri/src/commands/java2lce/payload.rs @@ -0,0 +1,683 @@ +use super::nbt; +const SAVE_FILE_VERSION_COMPRESSED_CHUNK_STORAGE: i16 = 8; +const SAVE_FILE_VERSION_CHUNK_INHABITED_TIME: i16 = 9; +const COMPRESSED_CHUNK_SECTION_HEIGHT: usize = 128; +const BLOCKS_PER_SECTION: usize = COMPRESSED_CHUNK_SECTION_HEIGHT * 16 * 16; +const NIBBLES_PER_SECTION: usize = BLOCKS_PER_SECTION / 2; +const FULL_CHUNK_BLOCKS: usize = 256 * 16 * 16; +const FULL_CHUNK_NIBBLES: usize = FULL_CHUNK_BLOCKS / 2; +const INDEX_TYPE_MASK: u16 = 0x0003; +const INDEX_TYPE_1BIT: u16 = 0x0000; +const INDEX_TYPE_2BIT: u16 = 0x0001; +const INDEX_TYPE_4BIT: u16 = 0x0002; +const INDEX_TYPE_0_OR_8BIT: u16 = 0x0003; +const INDEX_TYPE_0BIT_FLAG: u16 = 0x0004; +const SPARSE_ALL_ZERO_INDEX: u8 = 128; +const SPARSE_ALL_FIFTEEN_INDEX: u8 = 129; +fn read_be_i16(data: &[u8], off: usize) -> i16 { + i16::from_be_bytes([data[off], data[off + 1]]) +} + +fn read_be_i32(data: &[u8], off: usize) -> i32 { + i32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) +} + +fn read_be_i64(data: &[u8], off: usize) -> i64 { + i64::from_be_bytes([ + data[off], data[off + 1], data[off + 2], data[off + 3], data[off + 4], data[off + 5], + data[off + 6], data[off + 7], + ]) +} + +fn write_be_i16(out: &mut Vec, v: i16) { + out.extend_from_slice(&v.to_be_bytes()); +} + +fn write_be_i32(out: &mut Vec, v: i32) { + out.extend_from_slice(&v.to_be_bytes()); +} + +fn write_be_i64(out: &mut Vec, v: i64) { + out.extend_from_slice(&v.to_be_bytes()); +} + +fn get_compressed_tile_index(block: usize, tile: usize) -> usize { + let mut index = ((block & 0x180) << 6) | ((block & 0x060) << 4) | ((block & 0x01F) << 2); + index |= ((tile & 0x30) << 7) | ((tile & 0x0C) << 5) | (tile & 0x03); + index +} + +fn get_nibble_value(nibble_data: &[u8], xz: usize, y: usize) -> u8 { + let pos = (xz << 7) | y; + let slot = pos >> 1; + let part = pos & 1; + if slot >= nibble_data.len() { + return 0; + } + let b = nibble_data[slot]; + if part == 0 { + b & 0x0F + } else { + (b >> 4) & 0x0F + } +} + +fn set_nibble_value(nibble_data: &mut [u8], xz: usize, y: usize, value: u8) { + let pos = (xz << 7) | y; + let slot = pos >> 1; + let part = pos & 1; + if slot >= nibble_data.len() { + return; + } + let val = value & 0x0F; + if part == 0 { + nibble_data[slot] = (nibble_data[slot] & 0xF0) | val; + } else { + nibble_data[slot] = (nibble_data[slot] & 0x0F) | (val << 4); + } +} + +fn ensure_length(input: &[u8], expected_len: usize) -> Vec { + if input.len() == expected_len { + return input.to_vec(); + } + let mut v = vec![0u8; expected_len]; + let copy_len = input.len().min(expected_len); + v[..copy_len].copy_from_slice(&input[..copy_len]); + v +} + +fn extract_lower_block_section(full_blocks: &[u8]) -> Vec { + if full_blocks.len() == BLOCKS_PER_SECTION { + return full_blocks.to_vec(); + } + let mut lower = vec![0u8; BLOCKS_PER_SECTION]; + for xz in 0..256 { + let src_start = xz * 256; + let dst_start = xz * COMPRESSED_CHUNK_SECTION_HEIGHT; + let copy_len = COMPRESSED_CHUNK_SECTION_HEIGHT.min(full_blocks.len() - src_start.min(full_blocks.len())); + if src_start + copy_len <= full_blocks.len() && dst_start + copy_len <= BLOCKS_PER_SECTION { + lower[dst_start..dst_start + copy_len] + .copy_from_slice(&full_blocks[src_start..src_start + copy_len]); + } + } + lower +} + +fn extract_upper_block_section(full_blocks: &[u8]) -> Vec { + if full_blocks.len() < FULL_CHUNK_BLOCKS { + return vec![0u8; BLOCKS_PER_SECTION]; + } + let mut upper = vec![0u8; BLOCKS_PER_SECTION]; + for xz in 0..256 { + let src_start = xz * 256 + COMPRESSED_CHUNK_SECTION_HEIGHT; + let dst_start = xz * COMPRESSED_CHUNK_SECTION_HEIGHT; + upper[dst_start..dst_start + COMPRESSED_CHUNK_SECTION_HEIGHT].copy_from_slice(&full_blocks[src_start..src_start + COMPRESSED_CHUNK_SECTION_HEIGHT]); + } + upper +} + +fn extract_lower_nibble_section(full_nibbles: &[u8]) -> Vec { + let nibble_height = COMPRESSED_CHUNK_SECTION_HEIGHT / 2; + if full_nibbles.len() == NIBBLES_PER_SECTION { + return full_nibbles.to_vec(); + } + let mut lower = vec![0u8; NIBBLES_PER_SECTION]; + for xz in 0..256 { + let src_start = xz * nibble_height * 2; + let dst_start = xz * nibble_height; + let copy_len = nibble_height.min( + full_nibbles.len().saturating_sub(src_start), + ); + if copy_len > 0 { + lower[dst_start..dst_start + copy_len].copy_from_slice(&full_nibbles[src_start..src_start + copy_len]); + } + } + lower +} + +fn extract_upper_nibble_section(full_nibbles: &[u8]) -> Vec { + let nibble_height = COMPRESSED_CHUNK_SECTION_HEIGHT / 2; + if full_nibbles.len() < FULL_CHUNK_NIBBLES { + return vec![0u8; NIBBLES_PER_SECTION]; + } + let mut upper = vec![0u8; NIBBLES_PER_SECTION]; + for xz in 0..256 { + let src_start = xz * nibble_height * 2 + nibble_height; + let dst_start = xz * nibble_height; + upper[dst_start..dst_start + nibble_height] + .copy_from_slice(&full_nibbles[src_start..src_start + nibble_height]); + } + upper +} + +fn combine_block_sections(lower: &[u8], upper: &[u8]) -> Vec { + if lower.len() == FULL_CHUNK_BLOCKS { + return lower.to_vec(); + } + let mut combined = vec![0u8; FULL_CHUNK_BLOCKS]; + for xz in 0..256 { + let lower_start = xz * COMPRESSED_CHUNK_SECTION_HEIGHT; + let out_start = xz * 256; + let copy_len = COMPRESSED_CHUNK_SECTION_HEIGHT.min(lower.len().saturating_sub(lower_start)); + if copy_len > 0 { + combined[out_start..out_start + copy_len].copy_from_slice(&lower[lower_start..lower_start + copy_len]); + } + let upper_start = xz * COMPRESSED_CHUNK_SECTION_HEIGHT; + let out_upper = out_start + COMPRESSED_CHUNK_SECTION_HEIGHT; + let copy_len2 = COMPRESSED_CHUNK_SECTION_HEIGHT.min(upper.len().saturating_sub(upper_start)); + if copy_len2 > 0 { + combined[out_upper..out_upper + copy_len2] + .copy_from_slice(&upper[upper_start..upper_start + copy_len2]); + } + } + combined +} + +fn combine_nibble_sections(lower: &[u8], upper: &[u8]) -> Vec { + let nibble_height = COMPRESSED_CHUNK_SECTION_HEIGHT / 2; + if lower.len() == FULL_CHUNK_NIBBLES { + return lower.to_vec(); + } + let mut combined = vec![0u8; FULL_CHUNK_NIBBLES]; + for xz in 0..256 { + let lower_start = xz * nibble_height; + let out_start = xz * nibble_height * 2; + let copy_len = nibble_height.min(lower.len().saturating_sub(lower_start)); + if copy_len > 0 { + combined[out_start..out_start + copy_len] + .copy_from_slice(&lower[lower_start..lower_start + copy_len]); + } + let upper_start = xz * nibble_height; + let out_upper = out_start + nibble_height; + let copy_len2 = nibble_height.min(upper.len().saturating_sub(upper_start)); + if copy_len2 > 0 { + combined[out_upper..out_upper + copy_len2] + .copy_from_slice(&upper[upper_start..upper_start + copy_len2]); + } + } + combined +} + +pub fn encode_legacy_nbt(level: &nbt::NbtCompound) -> Vec { + let mut root = nbt::NbtCompound::new(""); + root.insert("Level", nbt::NbtValue::Compound(level.clone())); + nbt::write_nbt(&root) +} + +fn write_compressed_tile_storage(out: &mut Vec, blocks: &[u8]) { + let normalized = ensure_length(blocks, BLOCKS_PER_SECTION); + let mut blob = vec![0u8; 1024 + BLOCKS_PER_SECTION]; + let mut data_offset: usize = 0; + for block in 0..512 { + let index = (INDEX_TYPE_0_OR_8BIT | ((data_offset as u16) << 1)) as u16; + blob[block * 2..block * 2 + 2].copy_from_slice(&index.to_le_bytes()); + for tile in 0..64 { + blob[1024 + data_offset + tile] = normalized[get_compressed_tile_index(block, tile)]; + } + data_offset += 64; + } + + write_be_i32(out, blob.len() as i32); + out.extend_from_slice(&blob); +} + +fn write_empty_compressed_tile_storage(out: &mut Vec) { + let mut blob = vec![0u8; 1024]; + for block in 0..512 { + let index = (INDEX_TYPE_0_OR_8BIT | INDEX_TYPE_0BIT_FLAG) as u16; + blob[block * 2..block * 2 + 2].copy_from_slice(&index.to_le_bytes()); + } + write_be_i32(out, blob.len() as i32); + out.extend_from_slice(&blob); +} + +fn write_sparse_nibble_storage(out: &mut Vec, nibble_data: &[u8], supports_all_fifteen: bool) { + let normalized = ensure_length(nibble_data, NIBBLES_PER_SECTION); + let mut plane_indices = [0u8; 128]; + let mut planes: Vec> = Vec::new(); + for y in 0..128 { + let mut all_zero = true; + let mut all_fifteen = supports_all_fifteen; + let mut plane = vec![0u8; 128]; + let mut plane_cursor = 0; + for xz in 0..128 { + let first = get_nibble_value(&normalized, xz * 2, y); + let second = get_nibble_value(&normalized, xz * 2 + 1, y); + + if first != 0 || second != 0 { + all_zero = false; + } + if supports_all_fifteen && (first != 15 || second != 15) { + all_fifteen = false; + } + + plane[plane_cursor] = first | (second << 4); + plane_cursor += 1; + } + + if all_zero { + plane_indices[y] = SPARSE_ALL_ZERO_INDEX; + continue; + } + if supports_all_fifteen && all_fifteen { + plane_indices[y] = SPARSE_ALL_FIFTEEN_INDEX; + continue; + } + + plane_indices[y] = planes.len() as u8; + planes.push(plane); + } + + write_be_i32(out, planes.len() as i32); + out.extend_from_slice(&plane_indices); + for plane in &planes { + out.extend_from_slice(plane); + } +} + +fn write_empty_sparse_nibble_storage(out: &mut Vec, supports_all_fifteen: bool, fill_with_fifteen: bool) { + write_be_i32(out, 0); + let fill = if supports_all_fifteen && fill_with_fifteen { + SPARSE_ALL_FIFTEEN_INDEX + } else { + SPARSE_ALL_ZERO_INDEX + }; + let plane_indices = [fill; 128]; + out.extend_from_slice(&plane_indices); +} + +pub fn encode_compressed_storage(level: &nbt::NbtCompound) -> Vec { + let blocks = level.byte_array("Blocks").unwrap_or(&[]); + let data = level.byte_array("Data").unwrap_or(&[]); + let sky_light_raw = level.byte_array("SkyLight").unwrap_or(&[]); + let block_light_raw = level.byte_array("BlockLight").unwrap_or(&[]); + let height_map = level.byte_array("HeightMap").unwrap_or(&[]); + let biomes = level.byte_array("Biomes").unwrap_or(&[]); + let default_sky = vec![0xFF; NIBBLES_PER_SECTION]; + let default_light = vec![0u8; NIBBLES_PER_SECTION]; + let sky_light = if sky_light_raw.is_empty() { default_sky.as_slice() } else { sky_light_raw }; + let block_light = if block_light_raw.is_empty() { default_light.as_slice() } else { block_light_raw }; + let mut out = Vec::new(); + write_be_i16(&mut out, SAVE_FILE_VERSION_CHUNK_INHABITED_TIME); + write_be_i32(&mut out, level.int("xPos").unwrap_or(0)); + write_be_i32(&mut out, level.int("zPos").unwrap_or(0)); + write_be_i64(&mut out, level.long("LastUpdate").unwrap_or(0)); + write_be_i64(&mut out, level.long("InhabitedTime").unwrap_or(0)); + write_compressed_tile_storage(&mut out, &extract_lower_block_section(blocks)); + write_compressed_tile_storage(&mut out, &extract_upper_block_section(blocks)); + write_sparse_nibble_storage(&mut out, &extract_lower_nibble_section(data), false); + write_sparse_nibble_storage(&mut out, &extract_upper_nibble_section(data), false); + write_sparse_nibble_storage(&mut out, &extract_lower_nibble_section(sky_light), true); + write_sparse_nibble_storage(&mut out, &extract_upper_nibble_section(sky_light), true); + write_sparse_nibble_storage(&mut out, &extract_lower_nibble_section(block_light), true); + write_sparse_nibble_storage(&mut out, &extract_upper_nibble_section(block_light), true); + let hm = ensure_length(height_map, 256); + out.extend_from_slice(&hm[..256]); + write_be_i16(&mut out, level.short("TerrainPopulatedFlags").unwrap_or(0)); + let bio = ensure_length(biomes, 256); + out.extend_from_slice(&bio[..256]); + let dynamic_root = nbt::NbtCompound::new(""); + let dynamic_bytes = nbt::write_nbt(&dynamic_root); + out.extend_from_slice(&dynamic_bytes); + out +} + +fn is_compressed_chunk_storage(data: &[u8]) -> bool { + if data.len() < 2 + 4 + 4 + 8 { + return false; + } + let version = read_be_i16(data, 0); + version == SAVE_FILE_VERSION_COMPRESSED_CHUNK_STORAGE || version == SAVE_FILE_VERSION_CHUNK_INHABITED_TIME +} + +pub fn try_read_chunk_coordinates( + data: &[u8], +) -> Option<(i32, i32, bool)> { + if let Some((cx, cz, wrapped)) = try_read_legacy_level_coords(data) { + return Some((cx, cz, wrapped)); + } + + if is_compressed_chunk_storage(data) { + let cx = read_be_i32(data, 2); + let cz = read_be_i32(data, 6); + return Some((cx, cz, false)); + } + + None +} + +fn try_read_legacy_level_coords(data: &[u8]) -> Option<(i32, i32, bool)> { + if data.is_empty() || data[0] != 10 { + return None; + } + let compound = nbt::read_nbt(data).ok()?; + let (level, has_wrapper) = if let Some(nbt::NbtValue::Compound(c)) = compound.get("Level") { + (c, true) + } else { + (&compound, false) + }; + let cx = level.int("xPos")?; + let cz = level.int("zPos")?; + Some((cx, cz, has_wrapper)) +} + +pub fn force_chunk_coordinates(data: &[u8], expected_x: i32, expected_z: i32) -> Vec { + if data.is_empty() { + return Vec::new(); + } + + if let Ok((mut level, _)) = read_legacy_level(data) { + level.insert("xPos", nbt::NbtValue::Int(expected_x)); + level.insert("zPos", nbt::NbtValue::Int(expected_z)); + return encode_legacy_nbt(&level); + } + + if is_compressed_chunk_storage(data) { + let mut patched = data.to_vec(); + patched[2..6].copy_from_slice(&expected_x.to_be_bytes()); + patched[6..10].copy_from_slice(&expected_z.to_be_bytes()); + return patched; + } + + Vec::new() +} + +fn read_compressed_tile_storage(data: &[u8], offset: &mut usize) -> Result, String> { + let allocated_size = read_be_i32(data, *offset) as usize; + *offset += 4; + if allocated_size < 1024 || *offset + allocated_size > data.len() { + return Err("Invalid CompressedTileStorage payload.".into()); + } + + let blob = &data[*offset..*offset + allocated_size]; + *offset += allocated_size; + let data_region = &blob[1024..]; + let mut blocks = vec![0u8; BLOCKS_PER_SECTION]; + for block in 0..512 { + let block_index = u16::from_le_bytes([blob[block * 2], blob[block * 2 + 1]]); + let index_type = block_index & INDEX_TYPE_MASK; + if index_type == INDEX_TYPE_0_OR_8BIT { + if (block_index & INDEX_TYPE_0BIT_FLAG) != 0 { + let value = ((block_index >> 8) & 0xFF) as u8; + for tile in 0..64 { + blocks[get_compressed_tile_index(block, tile)] = value; + } + } else { + let data_offset = ((block_index >> 1) & 0x7FFE) as usize; + if data_offset + 64 > data_region.len() { + return Err("Invalid 8-bit CompressedTileStorage offset.".into()); + } + for tile in 0..64 { + blocks[get_compressed_tile_index(block, tile)] = data_region[data_offset + tile]; + } + } + continue; + } + + let bits_per_tile = match index_type { + INDEX_TYPE_1BIT => 1, + INDEX_TYPE_2BIT => 2, + INDEX_TYPE_4BIT => 4, + _ => return Err("Unsupported CompressedTileStorage index type.".into()), + }; + + let tile_type_count = 1usize << bits_per_tile; + let tile_type_mask = (tile_type_count - 1) as u8; + let index_shift = 3 - index_type as usize; + let index_mask_bits = (7 >> index_type) as usize; + let index_mask_bytes = (62 >> index_shift) as usize; + let packed_data_size = (8usize << index_type) as usize; + let data_offset_packed = ((block_index >> 1) & 0x7FFE) as usize; + if data_offset_packed + tile_type_count + packed_data_size > data_region.len() { + return Err("Invalid packed CompressedTileStorage offset.".into()); + } + + let tile_types = &data_region[data_offset_packed..data_offset_packed + tile_type_count]; + let packed = &data_region[data_offset_packed + tile_type_count..data_offset_packed + tile_type_count + packed_data_size]; + for tile in 0..64 { + let idx = (tile >> index_shift) & index_mask_bytes; + let bit = (tile & index_mask_bits) * bits_per_tile; + let palette_index = (packed[idx] >> bit) & tile_type_mask; + blocks[get_compressed_tile_index(block, tile)] = tile_types[palette_index as usize]; + } + } + + Ok(blocks) +} + +fn skip_compressed_tile_storage(data: &[u8], offset: &mut usize) -> Result<(), String> { + let allocated_size = read_be_i32(data, *offset) as usize; + *offset += 4; + if *offset + allocated_size > data.len() { + return Err("Invalid CompressedTileStorage payload.".into()); + } + *offset += allocated_size; + Ok(()) +} + +fn read_sparse_nibble_storage(data: &[u8], offset: &mut usize, supports_all_fifteen: bool) -> Result, String> { + let count = read_be_i32(data, *offset) as usize; + *offset += 4; + let storage_bytes = 128 + count * 128; + if count > data.len() || *offset + storage_bytes > data.len() { + return Err("Invalid SparseStorage payload.".into()); + } + + let blob = &data[*offset..*offset + storage_bytes]; + *offset += storage_bytes; + let plane_indices = &blob[..128]; + let plane_data = &blob[128..]; + let mut nibble_data = vec![0u8; NIBBLES_PER_SECTION]; + for y in 0..128 { + let plane_index = plane_indices[y]; + if plane_index == SPARSE_ALL_ZERO_INDEX { + continue; + } + if supports_all_fifteen && plane_index == SPARSE_ALL_FIFTEEN_INDEX { + for xz in 0..256 { + set_nibble_value(&mut nibble_data, xz, y, 15); + } + continue; + } + + let plane_offset = plane_index as usize * 128; + if plane_offset + 128 > plane_data.len() { + return Err("Invalid sparse plane index.".into()); + } + + let plane = &plane_data[plane_offset..plane_offset + 128]; + for xz in 0..128 { + let packed = plane[xz]; + set_nibble_value(&mut nibble_data, xz * 2, y, packed & 0x0F); + set_nibble_value(&mut nibble_data, xz * 2 + 1, y, (packed >> 4) & 0x0F); + } + } + + Ok(nibble_data) +} + +fn skip_sparse_nibble_storage(data: &[u8], offset: &mut usize) -> Result<(), String> { + let count = read_be_i32(data, *offset) as usize; + *offset += 4; + let storage_bytes = 128 + count * 128; + if count > data.len() || *offset + storage_bytes > data.len() { + return Err("Invalid SparseStorage payload.".into()); + } + *offset += storage_bytes; + Ok(()) +} + +fn read_sized_bytes(data: &[u8], offset: &mut usize, length: usize) -> Vec { + let end = (*offset + length).min(data.len()); + let result = data[*offset..end].to_vec(); + *offset += length; + result +} + +fn read_legacy_level(data: &[u8]) -> Result<(nbt::NbtCompound, bool), String> { + if data.is_empty() || data[0] != 10 { + return Err("Not NBT compound".into()); + } + let file = nbt::read_nbt(data)?; + let has_level = file.get("Level").is_some(); + let level = if let Some(nbt::NbtValue::Compound(c)) = file.get("Level") { + let mut cloned = c.clone(); + cloned.name = "Level".to_string(); + cloned + } else { + let mut cloned = file.clone(); + cloned.name = "Level".to_string(); + cloned + }; + Ok((level, has_level)) +} + +pub fn try_decode_to_legacy_nbt(data: &[u8]) -> Option> { + if let Ok((mut level, _)) = read_legacy_level(data) { + level.name = "Level".to_string(); + return Some(encode_legacy_nbt(&level)); + } + + if !is_compressed_chunk_storage(data) { + return None; + } + + decode_compressed_chunk_to_legacy_root(data).ok().map(|root| nbt::write_nbt(&root)) +} + +fn decode_compressed_chunk_to_legacy_root(data: &[u8]) -> Result { + let mut offset = 0; + let version = read_be_i16(data, offset); + offset = 2; + if version != SAVE_FILE_VERSION_COMPRESSED_CHUNK_STORAGE + && version != SAVE_FILE_VERSION_CHUNK_INHABITED_TIME + { + return Err(format!("Unsupported compressed chunk version: {}", version)); + } + + let chunk_x = read_be_i32(data, offset); + offset += 4; + let chunk_z = read_be_i32(data, offset); + offset += 4; + let last_update = read_be_i64(data, offset); + offset += 8; + let inhabited_time = if version >= SAVE_FILE_VERSION_CHUNK_INHABITED_TIME { + let v = read_be_i64(data, offset); + offset += 8; + v + } else { + 0 + }; + + let lower_blocks = read_compressed_tile_storage(data, &mut offset)?; + let upper_blocks = read_compressed_tile_storage(data, &mut offset)?; + let lower_data = read_sparse_nibble_storage(data, &mut offset, false)?; + let upper_data = read_sparse_nibble_storage(data, &mut offset, false)?; + let lower_sky = read_sparse_nibble_storage(data, &mut offset, true)?; + let upper_sky = read_sparse_nibble_storage(data, &mut offset, true)?; + let lower_block_light = read_sparse_nibble_storage(data, &mut offset, true)?; + let upper_block_light = read_sparse_nibble_storage(data, &mut offset, true)?; + let height_map = read_sized_bytes(data, &mut offset, 256); + let terrain_populated_flags = read_be_i16(data, offset); + offset += 2; + let biomes = read_sized_bytes(data, &mut offset, 256); + let dynamic_root = if offset < data.len() { + nbt::read_nbt(&data[offset..]).unwrap_or_default() + } else { + nbt::NbtCompound::default() + }; + + let mut level = nbt::NbtCompound::new("Level"); + level.insert("xPos", nbt::NbtValue::Int(chunk_x)); + level.insert("zPos", nbt::NbtValue::Int(chunk_z)); + level.insert("LastUpdate", nbt::NbtValue::Long(last_update)); + level.insert("InhabitedTime", nbt::NbtValue::Long(inhabited_time)); + level.insert( + "Blocks", + nbt::NbtValue::ByteArray(combine_block_sections(&lower_blocks, &upper_blocks)), + ); + level.insert( + "Data", + nbt::NbtValue::ByteArray(combine_nibble_sections(&lower_data, &upper_data)), + ); + level.insert( + "SkyLight", + nbt::NbtValue::ByteArray(combine_nibble_sections(&lower_sky, &upper_sky)), + ); + level.insert( + "BlockLight", + nbt::NbtValue::ByteArray(combine_nibble_sections( + &lower_block_light, + &upper_block_light, + )), + ); + level.insert("HeightMap", nbt::NbtValue::ByteArray(height_map)); + level.insert( + "TerrainPopulatedFlags", + nbt::NbtValue::Short(terrain_populated_flags), + ); + level.insert("Biomes", nbt::NbtValue::ByteArray(biomes)); + let entities = dynamic_root + .list("Entities") + .map(|l| nbt::NbtValue::List(l.to_vec())) + .unwrap_or_else(|| nbt::NbtValue::List(Vec::new())); + level.insert("Entities", entities); + let tile_entities = dynamic_root + .list("TileEntities") + .map(|l| nbt::NbtValue::List(l.to_vec())) + .unwrap_or_else(|| nbt::NbtValue::List(Vec::new())); + level.insert("TileEntities", tile_entities); + if let Some(tile_ticks) = dynamic_root.get("TileTicks") { + level.insert("TileTicks", tile_ticks.clone()); + } + + let mut root = nbt::NbtCompound::new(""); + root.insert("Level", nbt::NbtValue::Compound(level)); + Ok(root) +} + +pub fn try_get_compressed_chunk_nbt_offset(data: &[u8]) -> Option { + if !is_compressed_chunk_storage(data) { + return None; + } + + let mut offset = 0; + let version = read_be_i16(data, offset); + offset = 2; + if version != SAVE_FILE_VERSION_COMPRESSED_CHUNK_STORAGE + && version != SAVE_FILE_VERSION_CHUNK_INHABITED_TIME + { + return None; + } + + offset += 8; + if version >= SAVE_FILE_VERSION_CHUNK_INHABITED_TIME { + offset += 8; + } + + let r1 = skip_compressed_tile_storage(data, &mut offset); + if r1.is_err() { return None; } + let r2 = skip_compressed_tile_storage(data, &mut offset); + if r2.is_err() { return None; } + let r3 = skip_sparse_nibble_storage(data, &mut offset); + if r3.is_err() { return None; } + let r4 = skip_sparse_nibble_storage(data, &mut offset); + if r4.is_err() { return None; } + let r5 = skip_sparse_nibble_storage(data, &mut offset); + if r5.is_err() { return None; } + let r6 = skip_sparse_nibble_storage(data, &mut offset); + if r6.is_err() { return None; } + let r7 = skip_sparse_nibble_storage(data, &mut offset); + if r7.is_err() { return None; } + let r8 = skip_sparse_nibble_storage(data, &mut offset); + if r8.is_err() { return None; } + offset += 256; + offset += 2; + offset += 256; + if offset > data.len() { + return None; + } + + Some(offset) +} diff --git a/src-tauri/src/commands/java2lce/region.rs b/src-tauri/src/commands/java2lce/region.rs new file mode 100644 index 0000000..6ac5b2c --- /dev/null +++ b/src-tauri/src/commands/java2lce/region.rs @@ -0,0 +1,803 @@ +use std::collections::HashMap; +use std::fs; +use std::io::{Read, Write}; +use std::path::Path; +use flate2::read::ZlibDecoder; +use flate2::write::ZlibEncoder; +use flate2::Compression; +use super::nbt::{self, NbtCompound, NbtValue}; +const SECTOR_BYTES: usize = 4096; +pub struct JavaWorldReader { + world_path: String, + region_readers: HashMap, +} + +pub struct JavaRegionReader { + pub data: Vec, + pub offsets: [u32; 1024], +} + +impl JavaWorldReader { + pub fn new(world_path: &str) -> Self { + Self { + world_path: world_path.to_string(), + region_readers: HashMap::new(), + } + } + + pub fn read_level_dat(&self) -> Result { + let path = Path::new(&self.world_path).join("level.dat"); + let data = fs::read(&path).map_err(|e| format!("Failed to read level.dat: {}", e))?; + nbt::read_gzip_nbt(&data) + } + + pub fn get_region_dir(&self, dimension: &str) -> String { + if dimension.is_empty() { + Path::new(&self.world_path) + .join("region") + .to_string_lossy() + .to_string() + } else { + Path::new(&self.world_path) + .join(dimension) + .join("region") + .to_string_lossy() + .to_string() + } + } + + pub fn get_region_files(&self, dimension: &str) -> Vec<(i32, i32, String)> { + let mut result = Vec::new(); + let dir = self.get_region_dir(dimension); + let dir_path = Path::new(&dir); + if !dir_path.is_dir() { + return result; + } + + if let Ok(entries) = fs::read_dir(dir_path) { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + let lower = name.to_lowercase(); + if lower.ends_with(".mcr") || lower.ends_with(".mca") { + let parts: Vec<&str> = name.split('.').collect(); + if parts.len() == 4 && parts[0] == "r" { + if let (Ok(rx), Ok(rz)) =(parts[1].parse::(), parts[2].parse::()) + { + result.push((rx, rz, entry.path().to_string_lossy().to_string())); + } + } + } + } + } + + result + } + + pub fn is_anvil_world(&self) -> bool { + let region_dir = self.get_region_dir(""); + let dir_path = Path::new(®ion_dir); + if !dir_path.is_dir() { + return false; + } + if let Ok(entries) = fs::read_dir(dir_path) { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if name.to_lowercase().ends_with(".mca") { + return true; + } + } + } + false + } + + fn get_or_create_reader(&mut self, region_path: &str) -> Result<&JavaRegionReader, String> { + if !self.region_readers.contains_key(region_path) { + let reader = JavaRegionReader::open(region_path)?; + self.region_readers.insert(region_path.to_string(), reader); + } + Ok(self.region_readers.get(region_path).unwrap()) + } + + pub fn has_chunk(&mut self, region_path: &str, local_x: i32, local_z: i32) -> bool { + let reader = match self.get_or_create_reader(region_path) { + Ok(r) => r, + Err(_) => return false, + }; + let index = (local_x & 31) + (local_z & 31) * 32; + reader.offsets[index as usize] != 0 + } + + pub fn read_chunk_nbt( + &mut self, + region_path: &str, + local_x: i32, + local_z: i32, + ) -> Result, String> { + let reader = self.get_or_create_reader(region_path)?; + let index = (local_x & 31) + (local_z & 31) * 32; + let offset_entry = reader.offsets[index as usize]; + if offset_entry == 0 { + return Ok(None); + } + + let sector_offset = (offset_entry >> 8) as usize; + let chunk_pos = sector_offset * SECTOR_BYTES; + let data_len = reader.data.len(); + if chunk_pos + 5 > data_len { + return Ok(None); + } + + let length = u32::from_be_bytes([ + reader.data[chunk_pos], + reader.data[chunk_pos + 1], + reader.data[chunk_pos + 2], + reader.data[chunk_pos + 3], + ]) as usize; + let compression_type = reader.data[chunk_pos + 4]; + if length <= 1 { + return Ok(None); + } + + let compressed_length = length - 1; + if chunk_pos + 5 + compressed_length > data_len { + return Ok(None); + } + + let compressed_data = &reader.data[chunk_pos + 5..chunk_pos + 5 + compressed_length]; + let decompressed = match compression_type { + 1 => { + let mut decoder = flate2::read::GzDecoder::new(compressed_data); + let mut buf = Vec::new(); + decoder.read_to_end(&mut buf).map_err(|e| format!("gzip decompress failed: {}", e))?; + buf + } + 2 => { + let mut decoder = ZlibDecoder::new(compressed_data); + let mut buf = Vec::new(); + decoder + .read_to_end(&mut buf) + .map_err(|e| format!("zlib decompress failed: {}", e))?; + buf + } + _ => return Ok(None), + }; + + if decompressed.is_empty() { + return Ok(None); + } + + let compound = nbt::read_nbt(&decompressed)?; + Ok(Some(compound)) + } +} + +impl JavaRegionReader { + fn open(path: &str) -> Result { + let data = fs::read(path).map_err(|e| format!("Failed to read region file: {}", e))?; + let mut offsets = [0u32; 1024]; + for i in 0..1024 { + let off = i * 4; + if off + 4 > data.len() { + break; + } + offsets[i] = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]); + } + + Ok(Self { data, offsets }) + } + + pub fn open_from_bytes(data: &[u8]) -> Self { + let data = data.to_vec(); + let mut offsets = [0u32; 1024]; + for i in 0..1024 { + let off = i * 4; + if off + 4 > data.len() { + break; + } + offsets[i] = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]); + } + + Self { data, offsets } + } + + pub fn read_chunk( + &mut self, + local_x: i32, + local_z: i32, + ) -> Result>, String> { + let index = (local_x & 31) + (local_z & 31) * 32; + let offset_entry = self.offsets[index as usize]; + if offset_entry == 0 { + return Ok(None); + } + + let sector_offset = (offset_entry >> 8) as usize; + let chunk_pos = sector_offset * SECTOR_BYTES; + let data_len = self.data.len(); + if chunk_pos + 5 > data_len { + return Ok(None); + } + + let length = u32::from_be_bytes([ + self.data[chunk_pos], + self.data[chunk_pos + 1], + self.data[chunk_pos + 2], + self.data[chunk_pos + 3], + ]) as usize; + let compression_type = self.data[chunk_pos + 4]; + if length <= 1 { + return Ok(None); + } + + let compressed_length = length - 1; + if chunk_pos + 5 + compressed_length > data_len { + return Ok(None); + } + + let compressed_data = &self.data[chunk_pos + 5..chunk_pos + 5 + compressed_length]; + let decompressed = match compression_type { + 1 => { + let mut decoder = flate2::read::GzDecoder::new(compressed_data); + let mut buf = Vec::new(); + decoder + .read_to_end(&mut buf) + .map_err(|e| format!("gzip decompress failed: {}", e))?; + buf + } + 2 => { + let mut decoder = ZlibDecoder::new(compressed_data); + let mut buf = Vec::new(); + decoder + .read_to_end(&mut buf) + .map_err(|e| format!("zlib decompress failed: {}", e))?; + buf + } + _ => return Ok(None), + }; + + if decompressed.is_empty() { + return Ok(None); + } + + Ok(Some(decompressed)) + } +} + +pub struct JavaRegionFileWriter { + path: String, + buffer: Vec, + offsets: [i32; 1024], + timestamps: [i32; 1024], + next_sector: usize, +} + +impl JavaRegionFileWriter { + pub fn new(path: &str) -> Self { + let buffer = vec![0u8; SECTOR_BYTES * 2]; + let offsets = [0i32; 1024]; + let timestamps = [0i32; 1024]; + Self { + path: path.to_string(), + buffer, + offsets, + timestamps, + next_sector: 2, + } + } + + pub fn load_from_file(path: &str) -> Self { + if let Ok(data) = fs::read(path) { + let buffer = data.clone(); + let mut offsets = [0i32; 1024]; + let mut timestamps = [0i32; 1024]; + for i in 0..1024 { + let pos = i * 4; + if pos + 4 <= data.len() { + offsets[i] = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]); + } + } + + for i in 0..1024 { + let pos = SECTOR_BYTES + i * 4; + if pos + 4 <= data.len() { + timestamps[i] = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]); + } + } + + let mut max_sector = 2; + for &off in &offsets { + if off != 0 { + let sector = (off >> 8) as usize; + let count = (off & 0xFF) as usize; + let end = sector + count; + if end > max_sector { + max_sector = end; + } + } + } + + Self { + path: path.to_string(), + buffer, + offsets, + timestamps, + next_sector: max_sector, + } + } else { + Self::new(path) + } + } + + pub fn write_chunk(&mut self, local_x: i32, local_z: i32, uncompressed_nbt: &[u8]) { + if local_x < 0 || local_x > 31 || local_z < 0 || local_z > 31 { + return; + } + + let compressed = compress_zlib(uncompressed_nbt); + let payload_length = 1 + compressed.len(); + let total_length = 4 + payload_length; + let sectors_needed = (total_length + SECTOR_BYTES - 1) / SECTOR_BYTES; + if sectors_needed >= 256 { + return; + } + + let sector_start = self.next_sector; + self.next_sector += sectors_needed; + while self.buffer.len() < (sector_start + sectors_needed) * SECTOR_BYTES { + self.buffer.extend(std::iter::repeat(0u8).take(SECTOR_BYTES)); + } + + let write_pos = sector_start * SECTOR_BYTES; + self.buffer[write_pos..write_pos + 4] + .copy_from_slice(&(payload_length as u32).to_be_bytes()); + self.buffer[write_pos + 4] = 2; + self.buffer[write_pos + 5..write_pos + 5 + compressed.len()] + .copy_from_slice(&compressed); + + let padding = sectors_needed * SECTOR_BYTES - total_length; + if padding > 0 { + for i in 0..padding { + self.buffer[write_pos + total_length + i] = 0; + } + } + + let offset_index = (local_x & 31) + (local_z & 31) * 32; + self.offsets[offset_index as usize] = ((sector_start as i32) << 8) | (sectors_needed as i32); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i32; + self.timestamps[offset_index as usize] = now; + } + + pub fn save(&mut self) -> Result<(), String> { + while self.buffer.len() < self.next_sector * SECTOR_BYTES { + self.buffer.extend(std::iter::repeat(0u8).take(SECTOR_BYTES)); + } + + for i in 0..1024 { + let pos = i * 4; + self.buffer[pos..pos + 4].copy_from_slice(&(self.offsets[i] as u32).to_be_bytes()); + } + + for i in 0..1024 { + let pos = SECTOR_BYTES + i * 4; + self.buffer[pos..pos + 4] + .copy_from_slice(&(self.timestamps[i] as u32).to_be_bytes()); + } + + let total = self.next_sector * SECTOR_BYTES; + let data = &self.buffer[..total]; + if let Some(parent) = Path::new(&self.path).parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create region directory: {}", e))?; + } + + fs::write(&self.path, data) + .map_err(|e| format!("Failed to write region file: {}", e))?; + Ok(()) + } +} + +pub struct LceRegionFile { + filename: String, + data: Vec, + offsets: [i32; 1024], + timestamps: [i32; 1024], + region_x: i32, + region_z: i32, + sector_count: usize, +} + +impl LceRegionFile { + pub fn new(filename: &str) -> Self { + let (region_x, region_z) = parse_region_coordinates(filename); + let data = vec![0u8; SECTOR_BYTES * 2]; + Self { + filename: filename.to_string(), + data, + offsets: [0i32; 1024], + timestamps: [0i32; 1024], + region_x, + region_z, + sector_count: 2, + } + } + + pub fn write_chunk(&mut self, x: i32, z: i32, mut uncompressed_data: Vec) { + if x < 0 || x >= 32 || z < 0 || z >= 32 { + return; + } + if uncompressed_data.is_empty() { + return; + } + + uncompressed_data = force_chunk_coordinates( + &uncompressed_data, + self.region_x * 32 + x, + self.region_z * 32 + z, + ); + if uncompressed_data.is_empty() { + return; + } + + let compressed = compress_zlib_only(&uncompressed_data); + let total_size = 8 + compressed.len(); + let sectors_needed = (total_size + SECTOR_BYTES - 1) / SECTOR_BYTES; + if sectors_needed >= 256 { + return; + } + + let sector_number = self.sector_count; + self.sector_count += sectors_needed; + while self.data.len() < (sector_number + sectors_needed) * SECTOR_BYTES { + self.data.extend(std::iter::repeat(0u8).take(SECTOR_BYTES)); + } + + let write_pos = sector_number * SECTOR_BYTES; + let stored_length = compressed.len() as u32; + self.data[write_pos..write_pos + 4] + .copy_from_slice(&stored_length.to_le_bytes()); + self.data[write_pos + 4..write_pos + 8] + .copy_from_slice(&(uncompressed_data.len() as u32).to_le_bytes()); + self.data[write_pos + 8..write_pos + 8 + compressed.len()] + .copy_from_slice(&compressed); + + let padding = sectors_needed * SECTOR_BYTES - total_size; + if padding > 0 { + for i in 0..padding { + self.data[write_pos + total_size + i] = 0; + } + } + + let offset_index = (x + z * 32) as usize; + self.offsets[offset_index] = ((sector_number as i32) << 8) | (sectors_needed as i32); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i32; + self.timestamps[offset_index] = now; + } + + pub fn write_to_container(&mut self, container: &mut SaveDataContainer) { + let _sorted_entries: Vec<(usize, i32, i32)> = (0..1024) + .map(|i| (i, self.offsets[i], self.timestamps[i])) + .filter(|(_, off, _)| *off != 0) + .collect(); + + for i in 0..1024 { + let pos = i * 4; + self.data[pos..pos + 4].copy_from_slice(&(self.offsets[i] as u32).to_le_bytes()); + } + + for i in 0..1024 { + let pos = SECTOR_BYTES + i * 4; + self.data[pos..pos + 4] + .copy_from_slice(&(self.timestamps[i] as u32).to_le_bytes()); + } + + let total = self.sector_count * SECTOR_BYTES; + let region_bytes = self.data[..total].to_vec(); + let entry = container.create_file(&self.filename); + container.write_to_file(entry, ®ion_bytes); + } +} + +fn parse_region_coordinates(filename: &str) -> (i32, i32) { + let name = Path::new(filename) + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + let parts: Vec<&str> = name.split('.').collect(); + if parts.len() < 3 { + return (0, 0); + } + let rx = parts[parts.len() - 2].parse().unwrap_or(0); + let rz = parts[parts.len() - 1].parse().unwrap_or(0); + (rx, rz) +} + +pub fn force_chunk_coordinates(data: &[u8], expected_x: i32, expected_z: i32) -> Vec { + if data.is_empty() { + return Vec::new(); + } + + if let Ok(mut compound) = nbt::read_nbt(data) { + let has_level = compound.get("Level").is_some(); + if has_level { + if let Some(nbt::NbtValue::Compound(ref mut c)) = compound.get_mut("Level") { + c.insert("xPos", NbtValue::Int(expected_x)); + c.insert("zPos", NbtValue::Int(expected_z)); + } + } else { + compound.insert("xPos", NbtValue::Int(expected_x)); + compound.insert("zPos", NbtValue::Int(expected_z)); + } + + let mut root = NbtCompound::new(""); + root.insert("Level", NbtValue::Compound(compound)); + nbt::write_nbt(&root) + } else if data.len() >= 10 { + let version = i16::from_be_bytes([data[0], data[1]]); + if version == 8 || version == 9 { + let mut patched = data.to_vec(); + patched[2..6].copy_from_slice(&expected_x.to_be_bytes()); + patched[6..10].copy_from_slice(&expected_z.to_be_bytes()); + return patched; + } + Vec::new() + } else { + Vec::new() + } +} + +pub struct SaveDataContainer { + original_save_version: i16, + current_save_version: i16, + entries: Vec, + blob: Vec, + data_end: usize, +} + +pub struct SaveFileEntry { + pub name: String, + pub length: u32, + pub start_offset: u32, + pub last_mod: i64, + pub current_pointer: u32, +} + +impl SaveDataContainer { + pub const FILE_ENTRY_SIZE: usize = 144; + pub fn new(original_save_version: i16, current_save_version: i16) -> Self { + Self { + original_save_version, + current_save_version, + entries: Vec::new(), + blob: vec![0u8; 2 * 1024 * 1024], + data_end: 12, + } + } + + pub fn create_file(&mut self, name: &str) -> usize { + if let Some(idx) = self.entries.iter().position(|e| e.name == name) { + return idx; + } + let entry = SaveFileEntry { + name: name.to_string(), + length: 0, + start_offset: self.data_end as u32, + last_mod: 0, + current_pointer: self.data_end as u32, + }; + self.entries.push(entry); + self.entries.len() - 1 + } + + pub fn write_to_file(&mut self, index: usize, data: &[u8]) { + if data.is_empty() { + return; + } + + let needs_reinit = self.entries[index].length == 0; + if needs_reinit { + self.entries[index].start_offset = self.data_end as u32; + self.entries[index].current_pointer = self.data_end as u32; + } + + let write_pos = self.entries[index].current_pointer as usize; + let end_pos = write_pos + data.len(); + self.ensure_capacity(end_pos); + self.blob[write_pos..write_pos + data.len()].copy_from_slice(data); + self.entries[index].current_pointer += data.len() as u32; + let written = self.entries[index].current_pointer - self.entries[index].start_offset; + if written > self.entries[index].length { + self.entries[index].length = written; + } + + let entry_end = (self.entries[index].start_offset + self.entries[index].length) as usize; + if entry_end > self.data_end { + self.data_end = entry_end; + } + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64; + self.entries[index].last_mod = now; + } + + pub fn save(&mut self, output_path: &str) -> Result<(), String> { + self.write_header(); + let total_size = self.data_end + self.entries.len() * Self::FILE_ENTRY_SIZE; + self.ensure_capacity(total_size); + self.write_footer(); + let raw_blob = self.blob[..total_size].to_vec(); + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best()); + encoder + .write_all(&raw_blob) + .map_err(|e| format!("zlib compress failed: {}", e))?; + let compressed_data = encoder + .finish() + .map_err(|e| format!("zlib finish failed: {}", e))?; + + if let Some(parent) = Path::new(output_path).parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create output directory: {}", e))?; + } + + let mut file = fs::File::create(output_path) + .map_err(|e| format!("Failed to create output file: {}", e))?; + file.write_all(&(0u32).to_le_bytes()) + .map_err(|e| format!("write failed: {}", e))?; + file.write_all(&(total_size as u32).to_le_bytes()) + .map_err(|e| format!("write failed: {}", e))?; + file.write_all(&compressed_data) + .map_err(|e| format!("write failed: {}", e))?; + Ok(()) + } + + fn write_header(&mut self) { + self.blob[0..4] + .copy_from_slice(&(self.data_end as u32).to_le_bytes()); + self.blob[4..8] + .copy_from_slice(&(self.entries.len() as u32).to_le_bytes()); + self.blob[8..10] + .copy_from_slice(&self.original_save_version.to_le_bytes()); + self.blob[10..12] + .copy_from_slice(&self.current_save_version.to_le_bytes()); + } + + fn write_footer(&mut self) { + let mut sorted: Vec = (0..self.entries.len()).collect(); + sorted.sort_by_key(|&i| self.entries[i].start_offset); + let mut pos = self.data_end; + for &i in &sorted { + let entry = &self.entries[i]; + let mut name_bytes = vec![0u8; 128]; + let encoded: Vec = entry + .name + .encode_utf16() + .flat_map(|c| c.to_le_bytes()) + .collect(); + let copy_len = encoded.len().min(126); + name_bytes[..copy_len].copy_from_slice(&encoded[..copy_len]); + self.blob[pos..pos + 128].copy_from_slice(&name_bytes); + pos += 128; + self.blob[pos..pos + 4].copy_from_slice(&entry.length.to_le_bytes()); + pos += 4; + self.blob[pos..pos + 4].copy_from_slice(&entry.start_offset.to_le_bytes()); + pos += 4; + self.blob[pos..pos + 8].copy_from_slice(&entry.last_mod.to_le_bytes()); + pos += 8; + } + } + + fn ensure_capacity(&mut self, required: usize) { + if required <= self.blob.len() { + return; + } + let mut new_size = self.blob.len(); + while new_size < required { + new_size *= 2; + } + self.blob.resize(new_size, 0); + } +} + +pub fn compress_zlib(data: &[u8]) -> Vec { + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best()); + encoder.write_all(data).unwrap(); + encoder.finish().unwrap() +} + +pub fn compress_zlib_only(data: &[u8]) -> Vec { + compress_zlib(data) +} + +pub fn decompress_zlib(data: &[u8]) -> Result, String> { + let mut decoder = ZlibDecoder::new(data); + let mut buf = Vec::new(); + decoder.read_to_end(&mut buf).map_err(|e| format!("zlib decompress failed: {}", e))?; + Ok(buf) +} + +pub fn decompress_rle_zlib(data: &[u8], decompressed_size: usize) -> Result, String> { + let rle_data = decompress_zlib(data)?; + Ok(rle_decode(&rle_data, decompressed_size)) +} + +pub fn rle_encode(data: &[u8]) -> Vec { + let mut out = Vec::new(); + let mut i = 0; + while i < data.len() { + let current = data[i]; + i += 1; + let mut count = 1usize; + while i < data.len() && data[i] == current && count < 256 { + i += 1; + count += 1; + } + + if count <= 3 { + if current == 0xFF { + out.push(0xFF); + out.push((count - 1) as u8); + } else { + for _ in 0..count { + out.push(current); + } + } + } else { + out.push(0xFF); + out.push((count - 1) as u8); + out.push(current); + } + } + out +} + +pub fn rle_decode(data: &[u8], expected_size: usize) -> Vec { + let mut output = vec![0u8; expected_size]; + let mut in_pos = 0; + let mut out_pos = 0; + while in_pos < data.len() && out_pos < expected_size { + let current = data[in_pos]; + in_pos += 1; + if current == 0xFF { + if in_pos >= data.len() { + break; + } + let count = data[in_pos] as usize; + in_pos += 1; + if count < 3 { + let run = count + 1; + for _ in 0..run { + if out_pos < expected_size { + output[out_pos] = 0xFF; + out_pos += 1; + } + } + } else { + let run = count + 1; + if in_pos >= data.len() { + break; + } + let value = data[in_pos]; + in_pos += 1; + for _ in 0..run { + if out_pos < expected_size { + output[out_pos] = value; + out_pos += 1; + } + } + } + } else { + output[out_pos] = current; + out_pos += 1; + } + } + + output +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 38b63f4..e6ca211 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -4,6 +4,7 @@ pub mod dlc; pub mod download; pub mod file_dialogs; pub mod game; +pub mod java2lce; pub mod macos_setup; pub mod plugins; pub mod proxy_cmd; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 25151d8..cfc2f6a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -109,6 +109,8 @@ pub fn run() { game::restore_instance, commands::console2lce::import_world, commands::console2lce::import_lce_save, + commands::java2lce::java_to_lce, + commands::java2lce::lce_to_java, stun::stun_discover, relay::start_relay_proxy, relay::start_host_relay, diff --git a/src-tauri/src/networking/relay.rs b/src-tauri/src/networking/relay.rs index f4773c5..1046cf0 100644 --- a/src-tauri/src/networking/relay.rs +++ b/src-tauri/src/networking/relay.rs @@ -1,8 +1,6 @@ use tauri::State; -use tauri::webview::cookie::time::Duration; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; -use tokio::time::sleep; use tokio_util::sync::CancellationToken; use crate::state::ProxyGuard; const PROXY_ADDR: &str = "proxy.mclegacyedition.xyz:2052"; //neo: yeah bro im hardcoding it diff --git a/src/components/modals/ImportWorldModal.tsx b/src/components/modals/ImportWorldModal.tsx index 5cf5bb7..b6d2435 100644 --- a/src/components/modals/ImportWorldModal.tsx +++ b/src/components/modals/ImportWorldModal.tsx @@ -120,6 +120,70 @@ export default function ImportWorldModal({ } }; + const handleImportJava = async () => { + if (!targetInstanceId) return; + playPressSound(); + setIsImporting(true); + setError(""); + setStatus("Selecting source..."); + try { + setStatus("Selecting Java world folder..."); + const picked = await TauriService.pickFolder(); + if (!picked) { + setIsImporting(false); + return; + } + const worldName = deriveWorldName(picked); + setStatus("Converting Java world to LCE..."); + const instancePath = await TauriService.getInstancePath(targetInstanceId); + const saveDir = `${instancePath}/Windows64/GameHDD/${worldName}`; + await TauriService.javaToLce(picked, `${saveDir}/saveData.ms`); + setStatus(`Java world converted into "${targetInstanceName}"!`); + setTimeout(() => { + onClose(); + }, 2000); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : String(e)); + setStatus(""); + setIsImporting(false); + } + }; + + const handleExportJava = async () => { + if (!targetInstanceId) return; + playPressSound(); + setIsImporting(true); + setError(""); + setStatus("Selecting source..."); + try { + setStatus("Selecting saveData.ms file..."); + const picked = await TauriService.pickFile("Select saveData.ms", [ + "*.ms", + "*", + ]); + if (!picked) { + setIsImporting(false); + return; + } + setStatus("Selecting output folder for Java world..."); + const outputFolder = await TauriService.pickFolder(); + if (!outputFolder) { + setIsImporting(false); + return; + } + setStatus("Converting LCE save to Java world..."); + await TauriService.lceToJava(picked, outputFolder); + setStatus(`Java world exported to "${outputFolder}"!`); + setTimeout(() => { + onClose(); + }, 2000); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : String(e)); + setStatus(""); + setIsImporting(false); + } + }; + const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") { playBackSound(); @@ -204,6 +268,34 @@ export default function ImportWorldModal({ +

+ Java Edition world conversion: +

+
+ + +
+ {error && (
{error} diff --git a/src/services/TauriService.ts b/src/services/TauriService.ts index bc01e99..4ada2a8 100644 --- a/src/services/TauriService.ts +++ b/src/services/TauriService.ts @@ -427,4 +427,18 @@ export class TauriService { ): Promise { return invoke("import_lce_save", { inputPath, outputDir }); } + + static async javaToLce( + javaWorldPath: string, + outputMsPath: string, + ): Promise { + return invoke("java_to_lce", { javaWorldPath, outputMsPath }); + } + + static async lceToJava( + inputMsPath: string, + javaWorldOutput: string, + ): Promise { + return invoke("lce_to_java", { inputMsPath, javaWorldOutput }); + } }