mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-08-20 04:27:29 +00:00
Some checks failed
Nix / build (push) Has been cancelled
Co-authored-by: str1k3r <115313679+S1l3ntStr1ke87@users.noreply.github.com>
1704 lines
61 KiB
Rust
1704 lines
61 KiB
Rust
use std::collections::HashMap;
|
|
use super::block_mapping::{self, LegacyBlockState};
|
|
use super::nbt;
|
|
use super::payload;
|
|
const CHUNK_BLOCKS: usize = 32768;
|
|
const CHUNK_NIBBLES: usize = 16384;
|
|
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<i32>,
|
|
pub max_section_y: Option<i32>,
|
|
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<i32> = None;
|
|
let mut max_section_y: Option<i32> = 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<i32> {
|
|
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<u8> {
|
|
if let Some(bytes) = compound.byte_array(name) {
|
|
let mut v = bytes.to_vec();
|
|
if v.len() < default_size {
|
|
v.resize(default_size, 0);
|
|
} else {
|
|
v.truncate(default_size);
|
|
}
|
|
return v;
|
|
}
|
|
|
|
if let Some(ints) = compound.int_array(name) {
|
|
let mut v = vec![0u8; default_size];
|
|
let len = ints.len().min(default_size);
|
|
for (i, &val) in ints.iter().take(len).enumerate() {
|
|
v[i] = val.clamp(0, 255) as u8;
|
|
}
|
|
return v;
|
|
}
|
|
|
|
vec![0u8; default_size]
|
|
}
|
|
|
|
struct DecodedSection {
|
|
section_y: i32,
|
|
blocks: Vec<u8>,
|
|
data: Vec<u8>,
|
|
sky_light: Option<Vec<u8>>,
|
|
block_light: Option<Vec<u8>>,
|
|
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<u8>, Vec<u8>)> {
|
|
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<u8>, Vec<u8>)> {
|
|
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<i32>,
|
|
) -> (Vec<u8>, Vec<u8>, Vec<u8>, Vec<u8>) {
|
|
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<DecodedSection> = 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 > 7 {
|
|
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 >= 128 {
|
|
continue;
|
|
}
|
|
let flat_index = ((x * 16) + z) * 128 + 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,
|
|
global_section_shift: &mut Option<i32>,
|
|
) -> Vec<u8> {
|
|
let level =
|
|
build_legacy_chunk_level(root_tag, new_chunk_x, new_chunk_z, preserve_dynamic, global_section_shift);
|
|
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,
|
|
global_section_shift: &mut Option<i32>,
|
|
) -> Vec<u8> {
|
|
let level = build_legacy_chunk_level(root_tag, new_chunk_x, new_chunk_z, preserve_dynamic, global_section_shift);
|
|
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,
|
|
global_section_shift: &mut Option<i32>,
|
|
) -> 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, global_section_shift)
|
|
} 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;
|
|
if sky_light.iter().all(|&b| b == 0) {
|
|
sky_light.fill(0xFF);
|
|
}
|
|
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<nbt::NbtValue> = 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<nbt::NbtValue> {
|
|
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<nbt::NbtValue> {
|
|
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::<serde_json::Value>(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<nbt::NbtValue> {
|
|
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<nbt::NbtValue>, 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<nbt::NbtValue>) {
|
|
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<nbt::NbtValue>, 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<nbt::NbtValue> = Vec::new();
|
|
for section_y in 0..8 {
|
|
let mut palette_list: Vec<String> = Vec::new();
|
|
let mut palette_dict: HashMap<String, usize> = 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<nbt::NbtValue> = 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<nbt::NbtValue> = 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<String, String> = 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<String> = 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<String> {
|
|
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
|
|
)
|
|
}
|