mirror of
https://git.neolegacy.dev/neoStudiosLCE/neoLegacy.git
synced 2026-07-16 02:50:39 +00:00
185 lines
6.1 KiB
Python
185 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
import struct, sys, argparse
|
|
import xml.etree.ElementTree as ET
|
|
|
|
TAG_END = 0
|
|
TAG_BYTE = 1
|
|
TAG_SHORT = 2
|
|
TAG_INT = 3
|
|
TAG_LONG = 4
|
|
TAG_FLOAT = 5
|
|
TAG_DOUBLE = 6
|
|
TAG_BYTE_ARRAY = 7
|
|
TAG_STRING = 8
|
|
TAG_LIST = 9
|
|
TAG_COMPOUND = 10
|
|
TAG_INT_ARRAY = 11
|
|
|
|
class NBTParser:
|
|
def __init__(self, filename, endian='>'):
|
|
self.stream = open(filename, 'rb')
|
|
self.endian = endian
|
|
|
|
def read(self, fmt):
|
|
size = struct.calcsize(fmt)
|
|
data = self.stream.read(size)
|
|
if len(data) != size:
|
|
raise EOFError("Unexpected end of file")
|
|
return struct.unpack(fmt, data)[0]
|
|
|
|
def read_byte(self):
|
|
return self.read(self.endian + 'b')
|
|
def read_unsigned_byte(self):
|
|
return self.read(self.endian + 'B')
|
|
def read_short(self):
|
|
return self.read(self.endian + 'h')
|
|
def read_int(self):
|
|
return self.read(self.endian + 'i')
|
|
def read_long(self):
|
|
return self.read(self.endian + 'q')
|
|
def read_float(self):
|
|
return self.read(self.endian + 'f')
|
|
def read_double(self):
|
|
return self.read(self.endian + 'd')
|
|
|
|
def read_string(self):
|
|
length = self.read(self.endian + 'h') # unsigned short
|
|
data = self.stream.read(length)
|
|
return data.decode('utf-8')
|
|
|
|
def read_byte_array(self):
|
|
length = self.read(self.endian + 'i')
|
|
return self.stream.read(length)
|
|
|
|
def read_int_array(self):
|
|
length = self.read(self.endian + 'i')
|
|
return [self.read_int() for _ in range(length)]
|
|
|
|
def read_list(self):
|
|
elem_type = self.read_unsigned_byte()
|
|
count = self.read(self.endian + 'i')
|
|
return [self.read_tag_payload(elem_type) for _ in range(count)]
|
|
|
|
def read_compound(self):
|
|
compound = {}
|
|
while True:
|
|
tag_type = self.read_unsigned_byte()
|
|
if tag_type == TAG_END:
|
|
break
|
|
name = self.read_string()
|
|
compound[name] = self.read_tag_payload(tag_type)
|
|
return compound
|
|
|
|
def read_tag_payload(self, tag_type):
|
|
if tag_type == TAG_BYTE:
|
|
return self.read_byte()
|
|
elif tag_type == TAG_SHORT:
|
|
return self.read_short()
|
|
elif tag_type == TAG_INT:
|
|
return self.read_int()
|
|
elif tag_type == TAG_LONG:
|
|
return self.read_long()
|
|
elif tag_type == TAG_FLOAT:
|
|
return self.read_float()
|
|
elif tag_type == TAG_DOUBLE:
|
|
return self.read_double()
|
|
elif tag_type == TAG_BYTE_ARRAY:
|
|
return self.read_byte_array()
|
|
elif tag_type == TAG_STRING:
|
|
return self.read_string()
|
|
elif tag_type == TAG_LIST:
|
|
return self.read_list()
|
|
elif tag_type == TAG_COMPOUND:
|
|
return self.read_compound()
|
|
elif tag_type == TAG_INT_ARRAY:
|
|
return self.read_int_array()
|
|
else:
|
|
raise ValueError(f"Unknown tag type: {tag_type}")
|
|
|
|
def parse(self):
|
|
root_tag = self.read_unsigned_byte()
|
|
if root_tag != TAG_COMPOUND:
|
|
raise ValueError("Expected root TAG_Compound (0x0A)")
|
|
_ = self.read_string() # root name
|
|
data = self.read_compound()
|
|
self.stream.close()
|
|
return data
|
|
|
|
def structure_to_xml(data, xml_root_name="Structure"):
|
|
"""convert nbt data into xml"""
|
|
root = ET.Element(xml_root_name)
|
|
root.set("author", str(data.get("author", "Unknown")))
|
|
root.set("version", str(data.get("version", "")))
|
|
# Size
|
|
size = data.get("size", [0,0,0])
|
|
ET.SubElement(root, "Size", {
|
|
"width": str(size[0]), "height": str(size[1]), "length": str(size[2])
|
|
})
|
|
|
|
# Palette
|
|
pal_elem = ET.SubElement(root, "Palette")
|
|
palette = data.get("palette", [])
|
|
for i, entry in enumerate(palette):
|
|
attrs = {"index": str(i)}
|
|
if "Name" in entry:
|
|
attrs["name"] = entry["Name"]
|
|
block_elem = ET.SubElement(pal_elem, "Block", attrs)
|
|
# add properties as subelements
|
|
props = entry.get("Properties", {})
|
|
for prop_name, prop_value in props.items():
|
|
prop_elem = ET.SubElement(block_elem, "Property")
|
|
prop_elem.set("name", prop_name)
|
|
prop_elem.text = str(prop_value)
|
|
|
|
# Blocks
|
|
blocks_elem = ET.SubElement(root, "Blocks")
|
|
for blk in data.get("blocks", []):
|
|
pos = blk.get("pos", [])
|
|
state = blk.get("state", 0)
|
|
if pos and len(pos) == 3:
|
|
attrs = {"x": str(pos[0]), "y": str(pos[1]), "z": str(pos[2]),
|
|
"index": str(state)}
|
|
ET.SubElement(blocks_elem, "Block", attrs)
|
|
|
|
# Entities
|
|
ents = data.get("entities", [])
|
|
ents_elem = ET.SubElement(root, "Entities")
|
|
for ent in ents:
|
|
ent_elem = ET.SubElement(ents_elem, "Entity")
|
|
for key, val in ent.items():
|
|
sub = ET.SubElement(ent_elem, key)
|
|
sub.text = str(val)
|
|
return root
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Convert NBT structure files to XML")
|
|
parser.add_argument("inputs", nargs="+", help="Input NBT file(s)")
|
|
parser.add_argument("-l", "--little", action="store_true",
|
|
help="Assume little-endian NBT format")
|
|
parser.add_argument("-o", "--output", help="Output XML file (or directory)")
|
|
args = parser.parse_args()
|
|
|
|
endian = '<' if args.little else '>'
|
|
for infile in args.inputs:
|
|
try:
|
|
parser_nbt = NBTParser(infile, endian=endian)
|
|
data = parser_nbt.parse()
|
|
except Exception as e:
|
|
print(f"Error parsing {infile}: {e}", file=sys.stderr)
|
|
continue
|
|
|
|
xml_root = structure_to_xml(data)
|
|
tree = ET.ElementTree(xml_root)
|
|
outpath = args.output or infile + ".xml"
|
|
if args.output and len(args.inputs) > 1:
|
|
import os
|
|
os.makedirs(args.output, exist_ok=True)
|
|
base = os.path.basename(infile)
|
|
outpath = os.path.join(args.output, base + ".xml")
|
|
ET.indent(tree, space=" ", level=0)
|
|
tree.write(outpath, encoding="utf-8", xml_declaration=True)
|
|
print(f"Wrote XML to {outpath}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|