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