Initial commit

This commit is contained in:
Jeroen van der Hel 2025-05-27 19:23:34 +02:00
commit 27c1c0d53b
8 changed files with 11253 additions and 0 deletions

10196
.gitignore vendored Normal file

File diff suppressed because it is too large Load Diff

16
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Current File",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
}
]
}

BIN
13774.schematic Normal file

Binary file not shown.

BIN
18805.schem Normal file

Binary file not shown.

56
extract_java_textures.py Normal file
View File

@ -0,0 +1,56 @@
import zipfile
import os
def extract_java_textures(java_jar_path, output_dir):
"""
Extracts block textures from a Minecraft Java Edition client JAR.
Args:
java_jar_path (str): Full path to the Minecraft client JAR file (e.g., "C:/Users/YourUser/AppData/Roaming/.minecraft/versions/1.20.1/1.20.1.jar").
output_dir (str): Directory where the extracted textures will be saved.
"""
if not os.path.exists(java_jar_path):
print(f"Error: Java JAR file not found at {java_jar_path}")
return
texture_source_path_in_jar = 'assets/minecraft/textures/block/'
extracted_texture_path = os.path.join(output_dir, 'java_textures')
os.makedirs(extracted_texture_path, exist_ok=True)
print(f"Extracting textures from {java_jar_path} to {extracted_texture_path}...")
with zipfile.ZipFile(java_jar_path, 'r') as zip_ref:
for member in zip_ref.namelist():
if member.startswith(texture_source_path_in_jar) and member.endswith('.png'):
# Get the relative path within the block textures folder
relative_path = os.path.relpath(member, texture_source_path_in_jar)
# Construct the full output path
destination_path = os.path.join(extracted_texture_path, relative_path)
# Ensure the directory exists
os.makedirs(os.path.dirname(destination_path), exist_ok=True)
# Extract the file
with open(destination_path, 'wb') as outfile:
outfile.write(zip_ref.read(member))
print("Texture extraction complete.")
if __name__ == "__main__":
# --- IMPORTANT: Set these paths correctly ---
# Path to your Minecraft Java Edition JAR file
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!
# Directory where you want to save the extracted Java textures
output_textures_base_dir = "resources/" # Adjust as needed!
extract_java_textures(java_game_jar, output_textures_base_dir)
# Now, the 'java_textures' folder inside output_textures_base_dir
# will contain all the Java block PNGs.
# You can then directly map "minecraft:stone" to
# "C:/Users/NP110306/Desktop/Blueprint_Resources/java_textures/stone.png"
# (after handling the .png extension)

380
helpers.py Normal file
View File

@ -0,0 +1,380 @@
import re
from PIL import Image, ImageDraw, ImageFont # Make sure ImageDraw is imported
import os
import zipfile
TEXTURE_SIZE = 16 # Default Minecraft texture size in pixels
# --- 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
# --- Function to extract base block name from Java block string ---
def get_base_java_block_name(java_block_string, overrides):
"""
Extracts the base block name (e.g., 'stone', 'oak_log') from a full
Java block string (e.g., 'minecraft:stone', 'minecraft:oak_log[axis=y]'),
applying texture name overrides.
"""
# 1. Get the raw base name from the Java block string
name_without_prefix = java_block_string.replace("minecraft:", "")
raw_base_name = name_without_prefix.split('[')[0] # e.g., "spruce_trapdoor"
# 2. Apply override if defined, otherwise return the raw_base_name
overridden_name = overrides.get(raw_base_name, raw_base_name)
return overridden_name
def parse_block_states(java_block_string):
"""
Parses a Java block string to extract its state properties.
e.g., "minecraft:spruce_trapdoor[facing=north,half=bottom]" -> {'facing': 'north', 'half': 'bottom'}
"""
states = {}
match = re.search(r'\[(.*?)\]', java_block_string)
if match:
properties_str = match.group(1)
# Split by comma, then by equals sign
for prop_pair in properties_str.split(','):
if '=' in prop_pair:
key, value = prop_pair.split('=', 1)
states[key] = value
return states
def extract_java_textures(java_jar_path, output_dir):
"""
Extracts block textures from a Minecraft Java Edition client JAR.
Args:
java_jar_path (str): Full path to the Minecraft client JAR file.
output_dir (str): Directory where the extracted textures will be saved (e.g., 'java_textures' will be created inside this).
"""
if not os.path.exists(java_jar_path):
print(f"Error: Java JAR file not found at {java_jar_path}")
return
texture_source_path_in_jar = 'assets/minecraft/textures/block/'
extracted_texture_path = os.path.join(output_dir, 'java_textures') # Save to a subfolder
os.makedirs(extracted_texture_path, exist_ok=True)
print(f"Extracting textures from {java_jar_path} to {extracted_texture_path}...")
with zipfile.ZipFile(java_jar_path, 'r') as zip_ref:
for member in zip_ref.namelist():
if member.startswith(texture_source_path_in_jar) and member.endswith('.png'):
# Get the relative path within the block textures folder
relative_path = os.path.relpath(member, texture_source_path_in_jar)
# Construct the full output path
destination_path = os.path.join(extracted_texture_path, relative_path)
# Ensure the directory exists
os.makedirs(os.path.dirname(destination_path), exist_ok=True)
# Extract the file
with open(destination_path, 'wb') as outfile:
outfile.write(zip_ref.read(member))
print("Texture extraction complete.")
def create_arrow_texture(direction, size=TEXTURE_SIZE):
"""
Creates a red arrow pointing in the specified direction.
Args:
direction (str): 'north', 'south', 'east', 'west'.
size (int): The size of the square texture (e.g., 16).
Returns:
PIL.Image: An RGBA image of the arrow.
"""
arrow_img = Image.new('RGBA', (size, size), (0, 0, 0, 0)) # Transparent background
draw = ImageDraw.Draw(arrow_img)
line_color = (255, 0, 0, 255) # Red with full alpha
line_width = 1 # Pixels
center_x, center_y = size // 2, size // 2
# Define arrow head size relative to TEXTURE_SIZE
# head_size = size // 4 # Example: 4 pixels for 16x16 texture
head_size = size // 8 # Example: 2 pixels for 16x16 texture
if direction == 'north': # Arrow points up (towards decreasing Z)
draw.line([(center_x, size), (center_x, 0)], fill=line_color, width=line_width)
draw.polygon([(center_x, 0), (center_x - head_size, head_size), (center_x + head_size, head_size)], fill=line_color)
elif direction == 'south': # Arrow points down (towards increasing Z)
draw.line([(center_x, 0), (center_x, size)], fill=line_color, width=line_width)
draw.polygon([(center_x, size), (center_x - head_size, size - head_size), (center_x + head_size, size - head_size)], fill=line_color)
elif direction == 'west': # Arrow points left (towards decreasing X)
draw.line([(size, center_y), (0, center_y)], fill=line_color, width=line_width)
draw.polygon([(0, center_y), (head_size, center_y - head_size), (head_size, center_y + head_size)], fill=line_color)
elif direction == 'east': # Arrow points right (towards increasing X)
draw.line([(0, center_y), (size, center_y)], fill=line_color, width=line_width)
draw.polygon([(size, center_y), (size - head_size, center_y - head_size), (size - head_size, center_y + head_size)], fill=line_color)
else:
print(f"Warning: Unknown direction '{direction}' for arrow.")
return Image.new('RGBA', (size, size), (0, 0, 0, 0)) # Return transparent if direction is invalid
return arrow_img
def draw_door_state_overlay(texture: Image.Image, is_open: bool):
"""
Draws a small overlay symbol in the top-left corner of the texture to indicate door state.
- Green open rectangle for open
- Red closed rectangle for closed
"""
draw = ImageDraw.Draw(texture)
size = texture.width
overlay_size = size // 4 # 1/4 of texture size
x0, y0 = 2, 2
x1, y1 = x0 + overlay_size, y0 + overlay_size
if is_open:
# Draw a green open rectangle (open door shape)
draw.rectangle([x0, y0, x1, y1], outline=(0, 200, 0, 255), width=2)
# Draw a gap to indicate "open"
draw.line([x0 + overlay_size//2, y0, x1, y1], fill=(0, 200, 0, 255), width=2)
else:
# Draw a red closed rectangle (closed door shape)
draw.rectangle([x0, y0, x1, y1], outline=(200, 0, 0, 255), width=2)
# Draw a line to indicate "closed"
draw.line([x0, y0, x1, y1], fill=(200, 0, 0, 255), width=2)
def combine_door_halves(
bottom_texture: Image.Image,
top_texture: Image.Image,
half: str = "lower",
overlay_opacity: int = 128 # 0-255, 128 = 50% opacity
) -> Image.Image:
"""
Combines the bottom and top door textures into a single 16x16 image.
The 'other' half is shown with a semi-transparent white overlay.
Args:
bottom_texture: PIL.Image for the door bottom (8x16 or 16x16, will be cropped).
top_texture: PIL.Image for the door top (8x16 or 16x16, will be cropped).
half: "lower" or "upper" (which half is the real one)
overlay_opacity: Opacity for the overlay (0=transparent, 255=opaque)
Returns:
PIL.Image: The combined 16x16 image.
"""
# Ensure both textures are 16x16 (crop or resize if needed)
size = 16
bottom = bottom_texture.crop((0, 0, size, size))
top = top_texture.crop((0, 0, size, size))
# Create a new blank image
combined = Image.new("RGBA", (size, size), (0, 0, 0, 0))
if half == "lower":
# Place bottom half as normal
combined.paste(bottom, (0, 8))
# Place top half above, faded
faded_top = top.crop((0, 0, size, 8)).copy()
overlay = Image.new("RGBA", (size, 8), (255, 255, 255, overlay_opacity))
faded_top = Image.alpha_composite(faded_top, overlay)
combined.paste(faded_top, (0, 0), faded_top)
else: # half == "upper"
# Place top half as normal
combined.paste(top, (0, 0))
# Place bottom half below, faded
faded_bottom = bottom.crop((0, 8, size, 16)).copy()
overlay = Image.new("RGBA", (size, 8), (255, 255, 255, overlay_opacity))
faded_bottom = Image.alpha_composite(faded_bottom, overlay)
combined.paste(faded_bottom, (0, 8), faded_bottom)
return combined
def draw_door_overlay_symbols(
texture: Image.Image,
show_lock: bool = False,
hinge_side: str = "left"
):
"""
Draws overlay symbols in the top-left corner of the texture for doors:
- Optionally draws an unlocked (open) padlock (if show_lock is True)
- Optionally draws a small gray vertical bar on the hinge side ('left' or 'right')
Args:
texture (PIL.Image): The texture to draw on.
show_lock (bool): If True, draws an unlocked padlock.
hinge_side (str): 'left' or 'right' to indicate hinge side, or None.
"""
# Flip texture if hinge is right
if hinge_side =="right":
texture = texture.transpose(Image.FLIP_LEFT_RIGHT)
draw = ImageDraw.Draw(texture)
size = texture.width
overlay_size = size // 4 # 1/4 of texture size
x0, y0 = 2, 2
x1, y1 = x0 + overlay_size, y0 + overlay_size
# Draw hinge bar if specified
if hinge_side in ("left", "right"):
bar_width = max(1, overlay_size // 6)
bar_color = (120, 120, 120, 255) # Gray
if hinge_side == "left":
bar_x0 = x0
bar_x1 = x0 + bar_width
else: # "right"
bar_x0 = x1 - bar_width
bar_x1 = x1
draw.rectangle([bar_x0, y0, bar_x1, y1], fill=bar_color)
if show_lock:
# Draw an unlocked (open) padlock
lock_body_top = y1 - overlay_size // 3
lock_body_bottom = y1 - 2
lock_body_left = x0 + overlay_size // 4
lock_body_right = x1 - overlay_size // 4
# Body of the lock
draw.rectangle(
[lock_body_left, lock_body_top, lock_body_right, lock_body_bottom],
fill=(200, 200, 0, 255), outline=(120, 120, 0, 255), width=1
)
# Shackle of the lock (open arc)
shackle_box = [
lock_body_left - overlay_size // 8, lock_body_top - overlay_size // 4,
lock_body_right + overlay_size // 8, lock_body_top + overlay_size // 4
]
# Draw only part of the arc to look "open"
draw.arc(shackle_box, start=30, end=150, fill=(120, 120, 0, 255), width=2)
def render_grid(
textures_2d,
texture_size,
normal_padding=1,
bold_padding=3,
grid_line_color=(150, 150, 150, 255),
grid_line_bold_color=(80, 80, 80, 255),
grid_line_width=1,
grid_line_bold_width=3,
font_path="arial.ttf",
font_size=10,
margin=24
):
"""
Renders a 2D grid of textures with variable padding for gridlines, bold every 5th line,
and draws numbering outside the grid in an expanded canvas.
Args:
textures_2d: 2D list of PIL.Image objects (row-major: [z][x])
texture_size: Size of each texture square (pixels)
normal_padding: Space (pixels) between cells for normal gridlines
bold_padding: Space (pixels) between cells for every 5th (bold) gridline
grid_line_color: Color for normal grid lines
grid_line_bold_color: Color for every 5th grid line
grid_line_width: Width for normal grid lines
grid_line_bold_width: Width for every 5th grid line
font_path: Path to the font file
font_size: Font size for numbering
margin: Margin in pixels for numbering outside the grid
Returns:
PIL.Image: The rendered grid image with variable padding, gridlines, and outside numbering
"""
length = len(textures_2d)
width = len(textures_2d[0]) if length > 0 else 0
# Build padding arrays for columns and rows
paddings_col = []
for col in range(width + 1):
if col % 5 == 0:
paddings_col.append(bold_padding)
else:
paddings_col.append(normal_padding)
paddings_row = []
for row in range(length + 1):
if row % 5 == 0:
paddings_row.append(bold_padding)
else:
paddings_row.append(normal_padding)
# Calculate total image size (without margin)
img_width = width * texture_size + sum(paddings_col)
img_height = length * texture_size + sum(paddings_row)
# Create expanded image with margin for numbering (top and left)
expanded_width = img_width + margin
expanded_height = img_height + margin
out_img = Image.new('RGBA', (expanded_width, expanded_height), (255, 255, 255, 0))
draw = ImageDraw.Draw(out_img)
# Try to load a default font; fallback if not available
try:
font = ImageFont.truetype(font_path, font_size)
except Exception:
font = ImageFont.load_default()
# Precompute cumulative paddings for fast lookup
cum_paddings_col = [0]
for pad in paddings_col[:-1]:
cum_paddings_col.append(cum_paddings_col[-1] + pad + texture_size)
cum_paddings_row = [0]
for pad in paddings_row[:-1]:
cum_paddings_row.append(cum_paddings_row[-1] + pad + texture_size)
# Paste textures with variable padding, offset by margin
for z in range(length):
for x in range(width):
tx = margin + sum(paddings_col[:x+1]) + x * texture_size
ty = margin + sum(paddings_row[:z+1]) + z * texture_size
out_img.paste(textures_2d[z][x], (tx, ty), textures_2d[z][x])
# Draw vertical gridlines and outside numbering
x = margin
for col in range(width + 1):
is_bold = (col % 5 == 0)
color = grid_line_bold_color if is_bold else grid_line_color
line_width = grid_line_bold_width if is_bold else grid_line_width
pad = paddings_col[col]
offset = (pad - line_width) // 2
draw.line([(x + offset, margin), (x + offset, margin + img_height)], fill=color, width=line_width)
# Draw numbering outside the grid (above the grid)
if is_bold and col != 0 and x < margin + img_width:
text = str(col)
bbox = font.getbbox(text)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
text_x = x + offset - text_width // 2 + line_width // 2
text_y = (margin - text_height - 2) if (margin - text_height - 2) > 0 else 2
draw.rectangle(
[text_x, text_y, text_x + text_width, text_y + text_height],
fill=(255, 255, 255, 180)
)
draw.text((text_x, text_y), text, fill=(0, 0, 0, 255), font=font)
x += pad + (texture_size if col < width else 0)
# Draw horizontal gridlines and outside numbering
y = margin
for row in range(length + 1):
is_bold = (row % 5 == 0)
color = grid_line_bold_color if is_bold else grid_line_color
line_width = grid_line_bold_width if is_bold else grid_line_width
pad = paddings_row[row]
offset = (pad - line_width) // 2
draw.line([(margin, y + offset), (margin + img_width, y + offset)], fill=color, width=line_width)
# Draw numbering outside the grid (to the left of the grid)
if is_bold and row != 0 and y < margin + img_height:
text = str(row)
bbox = font.getbbox(text)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
text_x = (margin - text_width - 2) if (margin - text_width - 2) > 0 else 2
text_y = y + offset - text_height // 2 + line_width // 2
draw.rectangle(
[text_x, text_y, text_x + text_width, text_y + text_height],
fill=(255, 255, 255, 180)
)
draw.text((text_x, text_y), text, fill=(0, 0, 0, 255), font=font)
y += pad + (texture_size if row < length else 0)
return out_img

177
main copy.py Normal file
View File

@ -0,0 +1,177 @@
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("---------------------------------------")

428
main.py Normal file
View File

@ -0,0 +1,428 @@
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.")