440 lines
21 KiB
Python

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.")