import nbtlib import nbtlib.tag from nbtlib.tag import Int import gzip import os import sys # Import sys to print interpreter info NBTFormatError = getattr(nbtlib, 'NBTFormatError', None) if NBTFormatError is None: # Fallback for older nbtlib where it might have been in nbtlib.exceptions try: import nbtlib.exceptions NBTFormatError = nbtlib.exceptions.NBTFormatError except ImportError: NBTFormatError = Exception # Generic fallback if not found def parse_schem_file(filepath): """ Parses a .schem file and extracts its main components. Args: filepath (str): The path to the .schem file. Returns: dict: A dictionary containing schematic dimensions, block palette, block data, and potentially block entities and entities. Returns None if parsing fails. """ if not os.path.exists(filepath): print(f"Error: File not found at {filepath}") return None try: # nbtlib can directly load GZipped NBT files schematic = nbtlib.load(filepath) # Access the root compound tag root = schematic # --- Extract Header Information --- version = root["Version"] if version != Int(2): print(f"Warning: Schematic version is {version}. Expected 2 for .schem files. May not parse correctly.") width = root["Width"].value height = root["Height"].value length = root["Length"].value print(f"Schematic Dimensions: Width={width}, Height={height}, Length={length}") print(f"Schematic Version: {version}") # --- Extract Block Palette --- block_palette_nbt = root["BlockPalette"] block_palette = {} # The BlockPalette in .schem is a compound where keys are block string IDs # and values are integer IDs. We reverse this to map int ID -> string ID. for block_id_str, int_id_tag in block_palette_nbt.items(): block_palette[int_id_tag.value] = block_id_str print(f"\nBlock Palette ({len(block_palette)} entries):") # print(block_palette) # Uncomment to see the full palette # --- Extract Block Data --- # BlockData is typically a byte array or sometimes an int array block_data_raw = root["BlockData"].value # This is a bytes object # BlockData in .schem is often variable-length encoded (VLQ) # We need to decode it to get the actual block IDs # This part requires a custom VLQ decoder. nbtlib doesn't do this automatically. # A simple VLQ decoder (from WorldEdit docs / common implementations) # This function reads integers that are variable-length encoded. # Each byte, if its 7th bit is 1, indicates more bytes follow. # The actual value is in the lower 7 bits. decoded_block_ids = [] i = 0 while i < len(block_data_raw): value = 0 j = 0 while True: byte = block_data_raw[i] value |= (byte & 0x7F) << (7 * j) i += 1 if not (byte & 0x80): # If the 7th bit is 0, this is the last byte break j += 1 if j > 5: # Prevent infinite loops for corrupted data raise Exception("Corrupted VLQ data: too many bytes in integer") decoded_block_ids.append(value) print(f"\nDecoded Block Data Length: {len(decoded_block_ids)}") # print(decoded_block_ids[:10]) # Print first 10 decoded IDs # --- Reconstruct 3D block array (optional, but useful for processing) --- # Order is typically (Y * Length + Z) * Width + X blocks_3d = [[[None for _ in range(width)] for _ in range(length)] for _ in range(height)] for y in range(height): for z in range(length): for x in range(width): # Calculate the 1D index idx_1d = (y * length + z) * width + x if idx_1d < len(decoded_block_ids): palette_id = decoded_block_ids[idx_1d] block_string_id = block_palette.get(palette_id, "minecraft:unknown") blocks_3d[y][z][x] = block_string_id else: blocks_3d[y][z][x] = "minecraft:missing" # --- Extract Block Entities and Entities (if present) --- block_entities = root.get("BlockEntities", nbtlib.tag.List()) # List of Compound Tags entities = root.get("Entities", nbtlib.tag.List()) # List of Compound Tags print(f"\nNumber of Block Entities: {len(block_entities)}") if len(block_entities) > 0: # print("First Block Entity:", block_entities[0].pretty_tree()) # Pretty print first one pass # Or process them further print(f"Number of Entities: {len(entities)}") if len(entities) > 0: # print("First Entity:", entities[0].pretty_tree()) # Pretty print first one pass # Or process them further return { "version": version, "width": width, "height": height, "length": length, "block_palette": block_palette, "decoded_block_ids": decoded_block_ids, "blocks_3d": blocks_3d, "block_entities": block_entities, "entities": entities } except NBTFormatError as e: print(f"Error: Not a valid NBT or schematic file. {e}") except KeyError as e: print(f"Error: Missing expected NBT tag in schematic. Likely not a valid .schem file or corrupted. Missing tag: {e}") except Exception as e: print(f"An unexpected error occurred: {e}") return None # def import_schem_file(filepath): # nbtfile = nbtlib.load(filepath) # input() if __name__ == '__main__': # inputfile = "18805.schem" # import_schem_file(inputfile) schem_file_path = "18805.schem" parsed_data = parse_schem_file(schem_file_path) if parsed_data: print("\nSuccessfully parsed schematic data.") # You can now access and process the data: # print(parsed_data["blocks_3d"][0][0][0]) # Block at Y=0, Z=0, X=0 # print(parsed_data["block_palette"]) # Example: Print a top-down view of the first layer (Y=0) print("\n--- Top-Down View of Layer 0 (Y=0) ---") for z in range(parsed_data["length"]): row_blocks = [] for x in range(parsed_data["width"]): # Get the block string ID and truncate for display block_id = parsed_data["blocks_3d"][0][z][x] row_blocks.append(block_id.replace("minecraft:", "")[:6].ljust(6)) # Shorten for display print(" ".join(row_blocks)) print("---------------------------------------")