import nbtlib import nbtlib.tag import json import os import sys # Import sys to print interpreter info import zipfile import re # For parsing Java block strings from PIL import Image, ImageFont # Import Pillow for image manipulation from helpers import * # --- Constants for Blueprint Generation --- OUTPUT_BLUEPRINT_DIR = "blueprints" # Directory to save generated blueprints # --- Define the name for your missing texture file --- MISSING_TEXTURE_FILENAME = "missing_texture.png" # Make sure this matches your file name! # Adjusting for potential nbtlib API changes # In newer nbtlib, exceptions are directly under nbtlib 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 # type: ignore NBTFormatError = nbtlib.exceptions.NBTFormatError except ImportError: NBTFormatError = Exception # Generic fallback if not found # --- Add this dictionary at the top of your script, or after TEXTURE_SIZE constant --- # This maps specific Java base block names to the actual texture file name you want to use # (if they don't have their own dedicated texture file) TEXTURE_NAME_OVERRIDES = { "deepslate_tile_stairs": "deepslate_tiles", # Use deepslate_tiles.png for deepslate_tile_stairs "deepslate_tile_wall": "deepslate_tiles", # Use deepslate_tiles.png for deepslate_tile_wall "deepslate_tile_slab": "deepslate_tiles", # Use deepslate_tiles.png for deepslate_tile_slab "grass_block": "grass_block_side", "grass": "short_grass", # Use short_grass.png for grass blocks "tall_grass": "short_grass", # Use short_grass.png for tall_grass blocks "chain": "chain_block", "glass_pane": "glass", # Glass panes usually use the base glass texture "dark_oak_log": "dark_oak_log_top", # Sometimes logs have specific top/side textures #stairs "cobblestone_stairs": "cobblestone", "diorite_stairs": "diorite", "oak_stairs": "oak_planks", "spruce_stairs": "spruce_planks", "stone_stairs": "stone", #slabs "cobblestone_slab": "cobblestone", "diorite_slab": "diorite", "oak_slab": "oak_planks", "spruce_slab": "spruce_planks", "stone_slab": "stone", # Add more as needed based on your schematic's palette and texture files } # --- Function to load all Java block textures --- def load_all_java_block_textures(java_textures_root_dir, missing_texture_file=MISSING_TEXTURE_FILENAME): """ Loads all .png textures from the 'block' subfolder of extracted Java textures. Returns a dictionary mapping base block name (e.g., 'stone') to PIL Image objects. """ loaded_textures = {} block_textures_dir = os.path.join(java_textures_root_dir, 'java_textures') # Assuming this is where extract_java_textures saved them if not os.path.exists(block_textures_dir): print(f"Error: Java block textures directory not found at {block_textures_dir}") return loaded_textures # --- Load the designated missing texture first --- missing_texture_path = os.path.join(block_textures_dir, missing_texture_file) try: loaded_textures['__missing_texture_fallback__'] = Image.open(missing_texture_path).convert("RGBA") loaded_textures['__missing_texture_fallback__'].info['name'] = "missing_texture" print(f"Loaded missing texture fallback: {missing_texture_file}") except FileNotFoundError: print(f"Warning: Missing texture fallback '{missing_texture_file}' not found at {missing_texture_path}.") # Create a simple bright pink square if the file is not found loaded_textures['__missing_texture_fallback__'] = Image.new('RGBA', (TEXTURE_SIZE, TEXTURE_SIZE), (255, 0, 255, 255)) print("Using default bright pink square for missing textures.") except Exception as e: print(f"Warning: Could not load missing texture fallback '{missing_texture_file}': {e}") loaded_textures['__missing_texture_fallback__'] = Image.new('RGBA', (TEXTURE_SIZE, TEXTURE_SIZE), (255, 0, 255, 255)) print("Using default bright pink square for missing textures.") # Add a fully transparent texture for 'air' blocks (if you want air to be transparent, not pink) loaded_textures['air'] = Image.new('RGBA', (TEXTURE_SIZE, TEXTURE_SIZE), (0, 0, 0, 0)) # --- Load all other block textures --- for filename in os.listdir(block_textures_dir): if filename.endswith(".png"): base_name = filename.replace(".png", "") try: img_path = os.path.join(block_textures_dir, filename) img = Image.open(img_path).convert("RGBA") # Convert to RGBA for proper pasting img.info['name'] = base_name loaded_textures[base_name] = img # loaded_textures[base_name] = Image.open(img_path).convert("RGBA") # Convert to RGBA for proper pasting except Exception as e: print(f"Warning: Could not load texture {filename}: {e}") print(f"Loaded {len(loaded_textures)} Java block textures.") return loaded_textures 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: # Print debugging info for the nbtlib version being used at runtime print(f"--- Debug Info ---") print(f"Python executable: {sys.executable}") print(f"nbtlib module path: {nbtlib.__file__}") print(f"nbtlib version (runtime): {nbtlib.__version__}") print(f"------------------") schematic = nbtlib.load(filepath) root = schematic # In newer nbtlib, the loaded 'schematic' object acts as the root compound tag itself. # --- Extract Header Information --- version = int(root["Version"]) if version != 2: print(f"Warning: Schematic version is {version}. Expected 2 for .schem files. May not parse correctly.") width = int(root["Width"]) height = int(root["Height"]) length = int(root["Length"]) print(f"Schematic Dimensions: Width={width}, Height={height}, Length={length}") print(f"Schematic Version: {version}") # --- Extract Block Palette --- # Check for 'BlockPalette' first, then fall back to 'Palette' block_palette_nbt = root.get("BlockPalette") if block_palette_nbt is None: block_palette_nbt = root.get("Palette") if block_palette_nbt is None: raise KeyError("Neither 'BlockPalette' nor 'Palette' tag found in the schematic.") block_palette = {} for block_id_str, int_id_tag in block_palette_nbt.items(): block_palette[int(int_id_tag)] = block_id_str print(f"\nBlock Palette ({len(block_palette)} entries):") # --- Extract Block Data --- decoded_block_ids = [] bd = root["BlockData"].__array__() for b in bd: block_id = int(b) decoded_block_ids.append(block_id) print(f"\nDecoded Block Data Length: {len(decoded_block_ids)}") # --- Reconstruct 3D block array --- 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): 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()) entities = root.get("Entities", nbtlib.tag.List()) print(f"\nNumber of Block Entities: {len(block_entities)}") print(f"Number of Entities: {len(entities)}") 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. {e}") except Exception as e: print(f"An unexpected error occurred: {e}") return None if __name__ == "__main__": # --- IMPORTANT: Replace with the actual path to your .schem file --- schem_file_path = "18805.schem" BLUEPRINT_VERSION = "v2.4" # schem_file_path = "13774.schematic" # Path to your Minecraft Java Edition JAR file (e.g., from .minecraft/versions/1.20.1/1.20.1.jar) mc_java_version = "1.21.5" java_game_jar = f"C:/Users/NP110306/AppData/Roaming/.minecraft/versions/{mc_java_version}/{mc_java_version}.jar" # Adjust as needed! # Base directory where you want to save extracted textures and blueprints resources_base_dir = "resources/" # --- Step 1: Ensure Java textures are extracted --- # This only needs to be run once, or when you update your game version. # extract_java_textures(java_game_jar, resources_base_dir) # --- Step 2: Load all extracted Java textures into memory --- # This is the directory containing the 'java_textures' subfolder java_textures_loaded = load_all_java_block_textures(resources_base_dir, MISSING_TEXTURE_FILENAME) if not java_textures_loaded: print("Failed to load Java textures. Exiting blueprint generation.") sys.exit(1) # Exit if textures can't be loaded # Check if the missing texture fallback was actually loaded/created if '__missing_texture_fallback__' not in java_textures_loaded: print("Fatal Error: Missing texture fallback could not be loaded or created. Exiting.") sys.exit(1) # --- Step 3: Parse the schematic file --- parsed_data = parse_schem_file(schem_file_path) if parsed_data: print("\nSuccessfully parsed schematic data. Generating blueprints...") width = parsed_data["width"] height = parsed_data["height"] length = parsed_data["length"] blocks_3d = parsed_data["blocks_3d"] # Create output directory for blueprints if it doesn't exist os.makedirs(OUTPUT_BLUEPRINT_DIR, exist_ok=True) # Create a 'v2' subfolder for these enhanced blueprints os.makedirs(os.path.join(OUTPUT_BLUEPRINT_DIR, BLUEPRINT_VERSION), exist_ok=True) # --- Gridline and Padding Settings --- TEXTURE_SIZE = 16 # Space between cells for gridlines GRID_LINE_COLOR = (150, 150, 150, 255) GRID_LINE_BOLD_COLOR = (80, 80, 80, 255) GRID_LINE_WIDTH = 1 GRID_LINE_BOLD_WIDTH = 2 NORMAL_PADDING = GRID_LINE_WIDTH BOLD_PADDING = GRID_LINE_BOLD_WIDTH GRID_FONT_SIZE = 12 # Font size for grid numbers GRID_LAYER_MARGIN = 50 # Margin around the grid for layer numbers # --- Step 4: Generate layer-by-layer blueprints --- for y_layer in range(height): current_layer = blocks_3d[y_layer] # Prepare a 2D list of textures for this layer textures_2d = [] block_names_2d = [] for z in range(length): row_textures = [] row_names =[] for x in range(width): java_block_string = blocks_3d[y_layer][z][x] original_base_block_id = java_block_string.replace("minecraft:", "").split('[')[0] base_block_name_for_texture_lookup = get_base_java_block_name(java_block_string, TEXTURE_NAME_OVERRIDES) row_names.append(base_block_name_for_texture_lookup) block_states = parse_block_states(java_block_string) # Get the base texture to start with. IMPORTANT: Make a .copy()! if base_block_name_for_texture_lookup == 'air': texture_to_paste = java_textures_loaded['air'] else: # Use .copy() so we don't modify the original loaded texture in memory texture_to_paste = java_textures_loaded.get( base_block_name_for_texture_lookup, java_textures_loaded['__missing_texture_fallback__'] ).copy() if not texture_to_paste.info['name']: # If the texture doesn't have a name, set it to the base block name for debugging texture_to_paste.info['name'] = java_block_string # Store the name for debugging # --- Apply Slab Modifications --- if "_slab" in original_base_block_id: # Simple check for slab types (e.g., 'oak_slab', 'stone_slab') slab_type = block_states.get('type') # 'bottom', 'top', 'double' draw = ImageDraw.Draw(texture_to_paste) if slab_type == 'double': # Draw a white horizontal line across the middle draw.line([(0, TEXTURE_SIZE // 2), (TEXTURE_SIZE, TEXTURE_SIZE // 2)], fill=(255, 255, 255, 255), width=1) elif slab_type == 'top': # Make the bottom half transparent (visually representing top half of block) mask = Image.new('L', (TEXTURE_SIZE, TEXTURE_SIZE), 255) # Opaque by default mask_draw = ImageDraw.Draw(mask) mask_draw.rectangle([(0, TEXTURE_SIZE // 2), (TEXTURE_SIZE, TEXTURE_SIZE)], fill=0) # Make bottom half transparent (0) texture_to_paste.putalpha(mask) elif slab_type == 'bottom': # Make the top half transparent (visually representing bottom half of block) mask = Image.new('L', (TEXTURE_SIZE, TEXTURE_SIZE), 255) mask_draw = ImageDraw.Draw(mask) mask_draw.rectangle([(0, 0), (TEXTURE_SIZE, TEXTURE_SIZE // 2)], fill=0) # Make top half transparent (0) texture_to_paste.putalpha(mask) # --- Apply Stairs Modifications --- elif "_stairs" in original_base_block_id: # Simple check for stairs types (e.g., 'oak_stairs', 'stone_stairs') stair_half = block_states.get('half', 'bottom') # 'bottom', 'top' stair_facing = block_states.get('facing', 'north') # 'north', 'south', 'east', 'west' # Transparency for half (as requested) if stair_half == 'bottom': # Make upper-left quarter transparent (0,0 to TEXTURE_SIZE/2, TEXTURE_SIZE/2) mask = Image.new('L', (TEXTURE_SIZE, TEXTURE_SIZE), 255) mask_draw = ImageDraw.Draw(mask) mask_draw.rectangle([(0, 0), (TEXTURE_SIZE // 2, TEXTURE_SIZE // 2)], fill=0) texture_to_paste.putalpha(mask) elif stair_half == 'top': # Make bottom-left quarter transparent (0, TEXTURE_SIZE/2 to TEXTURE_SIZE/2, TEXTURE_SIZE) mask = Image.new('L', (TEXTURE_SIZE, TEXTURE_SIZE), 255) mask_draw = ImageDraw.Draw(mask) mask_draw.rectangle([(0, TEXTURE_SIZE // 2), (TEXTURE_SIZE // 2, TEXTURE_SIZE)], fill=0) texture_to_paste.putalpha(mask) # Red arrow for facing direction if stair_facing: arrow_img = create_arrow_texture(stair_facing, TEXTURE_SIZE) # Paste arrow at the center, using its alpha channel for transparency paste_x = (TEXTURE_SIZE - arrow_img.width) // 2 paste_y = (TEXTURE_SIZE - arrow_img.height) // 2 texture_to_paste.paste(arrow_img, (paste_x, paste_y), arrow_img) # Paste the (potentially modified) texture onto the layer image # --- Apply Door Modifications --- elif "_door" in original_base_block_id: material = original_base_block_id.replace("_door", "") door_half = block_states.get('half', 'lower') # 'lower' or 'upper' door_facing = block_states.get('facing', 'north') # 'north', 'south', 'east', 'west' door_hinge = block_states.get('hinge', 'left') # Default to 'left' if not specified door_open = True if block_states.get('open') == "true" else False # True if door is open, False if closed # Load both halves bottom_texture = java_textures_loaded.get( f"{material}_door_bottom", java_textures_loaded['__missing_texture_fallback__'] ).copy() top_texture = java_textures_loaded.get( f"{material}_door_top", java_textures_loaded['__missing_texture_fallback__'] ).copy() # Combine them into a single texture texture_to_paste = combine_door_halves( bottom_texture, top_texture, half=door_half, overlay_opacity=128 # 50% opacity for the faded half ) if not texture_to_paste.info.get('name'): texture_to_paste.info['name'] = f"{material}_door_{door_half}_combined" # Red arrow for facing direction if door_facing: arrow_img = create_arrow_texture(door_facing, TEXTURE_SIZE) # Paste arrow at the center, using its alpha channel for transparency paste_x = (TEXTURE_SIZE - arrow_img.width) // 2 paste_y = (TEXTURE_SIZE - arrow_img.height) // 2 texture_to_paste.paste(arrow_img, (paste_x, paste_y), arrow_img) # Only show unlocked padlock if door is open, always show hinge bar draw_door_overlay_symbols( texture_to_paste, show_lock=door_open, hinge_side=door_hinge ) row_textures.append(texture_to_paste) textures_2d.append(row_textures) block_names_2d.append(row_names) # --- Draw Grid Lines --- layer_image_with_grid_and_numbers = render_grid( textures_2d, texture_size=TEXTURE_SIZE, normal_padding=NORMAL_PADDING, bold_padding=BOLD_PADDING, grid_line_color=GRID_LINE_COLOR, grid_line_bold_color=GRID_LINE_BOLD_COLOR, grid_line_width=GRID_LINE_WIDTH, grid_line_bold_width=GRID_LINE_BOLD_WIDTH, font_size=GRID_FONT_SIZE, # or your preferred size margin=GRID_LAYER_MARGIN # or your preferred margin ) # Add legend to the left layer_with_legend = draw_dynamic_block_legend_left( layer_image_with_grid_and_numbers, textures_2d, java_textures_loaded, block_names_2d, font_size=14, margin=24 ) # Save the generated layer image output_filepath = os.path.join(OUTPUT_BLUEPRINT_DIR, BLUEPRINT_VERSION, f"blueprint_layer_{y_layer:03d}.png") layer_with_legend.save(output_filepath) print(f"Generated blueprint for layer {y_layer} at {output_filepath}") print("\nBlueprint generation complete!") else: print("Schematic parsing failed. Cannot generate blueprints.")