429 lines
20 KiB
Python
429 lines
20 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.3"
|
|
# 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 = []
|
|
|
|
for z in range(length):
|
|
row_textures = []
|
|
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)
|
|
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)
|
|
|
|
# --- 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
|
|
)
|
|
|
|
# Save the generated layer image
|
|
output_filepath = os.path.join(OUTPUT_BLUEPRINT_DIR, BLUEPRINT_VERSION, f"blueprint_layer_{y_layer:03d}.png")
|
|
layer_image_with_grid_and_numbers.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.")
|