initial commit

Functioning build
This commit is contained in:
Robbie Gordon
2026-03-07 17:15:28 -08:00
committed by GitHub
parent 90241f7724
commit efad2069c7
18 changed files with 3662 additions and 0 deletions

388
MLCE Converter.py Normal file
View File

@@ -0,0 +1,388 @@
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import json
import os
from pathlib import Path
from PIL import Image, ImageTk
# ── THEME ──────────────────────────────────────────────────────────────────
BG_DARK = "#2D2D30"
BG_PANEL = "#1E1E1E"
FG_MAIN = "#FFFFFF"
FG_GRAY = "#808080"
ACCENT = "#007ACC"
BTN_GREEN = "#3C8527"
BTN_RES = "#4A4A4F"
BTN_RES_ON= "#CC7A00"
# Base canvas sizes in 16x tile units (width_tiles, height_tiles)
# Multiply by tile_px at build time to get pixel size
CANVAS_TILES = {
"terrain": (16, 32),
"items": (16, 16),
"particles": (8, 8),
}
_preview_img = None
projects = {
"terrain": {"json_path": "", "layout": [], "final_map": {}},
"items": {"json_path": "", "layout": [], "final_map": {}},
"particles": {"json_path": "", "layout": [], "final_map": {}},
}
source_dir = {"v": ""}
current_scale = {"v": 1} # 1=16x 2=32x 4=64x
TAB_ORDER = ["terrain", "items", "particles"]
# ── ROOT ───────────────────────────────────────────────────────────────────
root = tk.Tk()
root.title("Texture Manager v5.5")
root.geometry("1000x800")
root.minsize(800, 600)
root.configure(bg=BG_DARK)
# ── TTK STYLES ─────────────────────────────────────────────────────────────
style = ttk.Style()
style.theme_use("default")
style.configure("Dark.Treeview",
background=BG_PANEL, foreground=FG_MAIN,
fieldbackground=BG_PANEL, rowheight=22,
font=("Segoe UI", 9))
style.configure("Dark.Treeview.Heading",
background="#3C3C3C", foreground=FG_MAIN,
font=("Segoe UI", 9, "bold"), relief="flat")
style.map("Dark.Treeview",
background=[("selected", ACCENT)],
foreground=[("selected", FG_MAIN)])
style.configure("Dark.TNotebook",
background=BG_DARK, borderwidth=0, tabmargins=0)
style.configure("Dark.TNotebook.Tab",
background="#3C3C3C", foreground=FG_GRAY,
padding=[18, 7], font=("Segoe UI", 9, "bold"))
style.map("Dark.TNotebook.Tab",
background=[("selected", ACCENT)],
foreground=[("selected", FG_MAIN)])
# ═══════════════════════════════════════════════════════════════════════════
# TOP BAR
# ═══════════════════════════════════════════════════════════════════════════
top_bar = tk.Frame(root, bg=BG_PANEL, height=46)
top_bar.pack(side="top", fill="x")
top_bar.pack_propagate(False)
def pick_library():
d = filedialog.askdirectory(title="Select Texture Library Folder")
if d:
source_dir["v"] = d
btn_lib.config(text=f"Library: {Path(d).name}")
btn_lib = tk.Button(top_bar, text="1. SELECT LIBRARY",
bg="#3C3C3C", fg=FG_MAIN,
activebackground="#505050", activeforeground=FG_MAIN,
relief="flat", font=("Segoe UI", 9, "bold"),
command=pick_library)
btn_lib.pack(side="left", padx=(8, 6), pady=7, ipady=5, ipadx=10)
json_btns = {}
for ptype in TAB_ORDER:
def make_json_cmd(pt):
def cmd():
f = filedialog.askopenfilename(
title=f"Select {pt.upper()} JSON",
filetypes=[("JSON Files", "*.json")])
if f:
with open(f, "r", encoding="utf-8") as fh:
projects[pt]["layout"] = json.load(fh)
projects[pt]["json_path"] = f
projects[pt]["final_map"] = {}
json_btns[pt].config(
text=f"{pt.upper()} JSON: {Path(f).name}",
fg="#90EE90")
refresh_list(pt)
return cmd
b = tk.Button(top_bar,
text=f"{ptype.upper()} JSON: ---",
bg="#3C3C3C", fg=FG_GRAY,
activebackground="#505050", activeforeground=FG_MAIN,
relief="flat", font=("Segoe UI", 9),
command=make_json_cmd(ptype))
b.pack(side="left", padx=4, pady=7, ipady=5, ipadx=8)
json_btns[ptype] = b
tk.Frame(top_bar, bg="#555555", width=2).pack(
side="left", fill="y", pady=8, padx=6)
res_btns = {}
def set_resolution(scale):
current_scale["v"] = scale
labels = {1: "16x", 2: "32x", 4: "64x"}
for s, btn in res_btns.items():
if s == scale:
btn.config(bg=BTN_RES_ON, fg=FG_MAIN, relief="sunken")
else:
btn.config(bg=BTN_RES, fg=FG_GRAY, relief="flat")
root.title(f"Texture Manager v5.5 [{labels[scale]}]")
for scale, label in [(1, "16x"), (2, "32x"), (4, "64x")]:
def make_res_cmd(s):
return lambda: set_resolution(s)
b = tk.Button(top_bar, text=label,
bg=BTN_RES, fg=FG_GRAY,
activebackground="#666666", activeforeground=FG_MAIN,
relief="flat", font=("Segoe UI", 9, "bold"), width=4,
command=make_res_cmd(scale))
b.pack(side="left", padx=2, pady=7, ipady=5)
res_btns[scale] = b
set_resolution(1)
# ═══════════════════════════════════════════════════════════════════════════
# LOAD TILE
# Crops to a square first frame (handles animated strips of any height),
# then resizes to exactly tile_px × tile_px using nearest neighbour.
# ═══════════════════════════════════════════════════════════════════════════
def load_tile(fpath, tile_px):
img = Image.open(fpath).convert("RGBA")
w, h = img.size
# Determine the native tile size: smallest of w and h
# (animated strips are always taller than wide)
native = min(w, h)
# Crop to first frame (top-left native×native square)
if w != native or h != native:
img = img.crop((0, 0, native, native))
# Scale to target tile size
if img.size != (tile_px, tile_px):
img = img.resize((tile_px, tile_px), Image.NEAREST)
return img
# ═══════════════════════════════════════════════════════════════════════════
# NOTEBOOK TABS
# ═══════════════════════════════════════════════════════════════════════════
notebook = ttk.Notebook(root, style="Dark.TNotebook")
notebook.pack(side="top", fill="both", expand=True, padx=10, pady=(6, 0))
trees = {}
previews = {}
for ptype in TAB_ORDER:
tab = tk.Frame(notebook, bg=BG_DARK)
notebook.add(tab, text=f" {ptype.upper()} ")
sv = tk.StringVar()
se = tk.Entry(tab, textvariable=sv,
bg=BG_PANEL, fg=FG_MAIN, insertbackground=FG_MAIN,
relief="flat", font=("Segoe UI", 10))
se.pack(side="top", fill="x", padx=0, pady=(0, 4), ipady=4)
projects[ptype]["search_var"] = sv
sv.trace_add("write", lambda *_, pt=ptype: refresh_list(pt))
pane = tk.PanedWindow(tab, orient=tk.HORIZONTAL,
bg=BG_DARK, sashwidth=5, sashrelief="flat")
pane.pack(fill="both", expand=True)
lf = tk.Frame(pane, bg=BG_PANEL)
pane.add(lf, width=620, minsize=300)
tree = ttk.Treeview(lf, columns=("block", "source"),
show="headings", style="Dark.Treeview",
selectmode="browse")
tree.heading("block", text="BLOCK TYPE")
tree.heading("source", text="SOURCE FILE")
tree.column("block", width=250, anchor="w")
tree.column("source", width=350, anchor="w")
vsb = ttk.Scrollbar(lf, orient="vertical", command=tree.yview)
tree.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
tree.pack(side="left", fill="both", expand=True)
trees[ptype] = tree
rf = tk.Frame(pane, bg=BG_DARK)
pane.add(rf, minsize=200)
pc = tk.Canvas(rf, bg="black", highlightthickness=0)
pc.place(x=10, y=10, relwidth=1.0, width=-20, height=310)
previews[ptype] = pc
def make_auto(pt):
def do_auto():
if not source_dir["v"]:
messagebox.showwarning("No Library",
"Please select a library folder first.")
return
png_files = {}
for r, _, files in os.walk(source_dir["v"]):
for f in files:
if f.lower().endswith(".png"):
base = Path(f).stem
if base not in png_files:
png_files[base] = os.path.join(r, f)
matched = 0
for i, obj in enumerate(projects[pt]["layout"]):
name = obj.get("Name") or obj.get("n", "")
if name in png_files:
projects[pt]["final_map"][i] = png_files[name]
matched += 1
refresh_list(pt)
messagebox.showinfo("Auto-Sync Complete",
f"Matched {matched} of {len(projects[pt]['layout'])} entries.")
return do_auto
tk.Button(rf, text="AUTO-SYNC FROM LIBRARY",
bg=ACCENT, fg=FG_MAIN,
activebackground="#005FA3", activeforeground=FG_MAIN,
relief="flat", font=("Segoe UI", 10, "bold"),
command=make_auto(ptype)).place(
x=10, y=328, relwidth=1.0, width=-20, height=52)
def make_sel(pt):
def on_sel(event):
global _preview_img
sel = trees[pt].selection()
if not sel:
return
idx = int(sel[0])
previews[pt].delete("all")
if idx not in projects[pt]["final_map"]:
return
try:
img = load_tile(projects[pt]["final_map"][idx], 16)
cw = previews[pt].winfo_width() or 310
ch = previews[pt].winfo_height() or 310
sc = max(1, min(cw // img.width, ch // img.height))
img = img.resize(
(img.width * sc, img.height * sc), Image.NEAREST)
_preview_img = ImageTk.PhotoImage(img)
ox = (cw - img.width) // 2
oy = (ch - img.height) // 2
previews[pt].create_image(ox, oy, anchor="nw",
image=_preview_img)
except Exception:
pass
return on_sel
tree.bind("<<TreeviewSelect>>", make_sel(ptype))
def make_dbl(pt):
def on_dbl(event):
sel = trees[pt].selection()
if not sel:
return
idx = int(sel[0])
path = filedialog.askopenfilename(
title="Select PNG",
filetypes=[("PNG Files", "*.png")])
if path:
projects[pt]["final_map"][idx] = path
refresh_list(pt)
trees[pt].selection_set(str(idx))
return on_dbl
tree.bind("<Double-1>", make_dbl(ptype))
# ═══════════════════════════════════════════════════════════════════════════
# REFRESH LIST
# ═══════════════════════════════════════════════════════════════════════════
def refresh_list(ptype=None):
if ptype is None:
ptype = TAB_ORDER[notebook.index(notebook.select())]
tree = trees[ptype]
proj = projects[ptype]
filt = proj["search_var"].get().lower()
tree.delete(*tree.get_children())
for i, obj in enumerate(proj["layout"]):
display = (obj.get("DisplayName") or
obj.get("n") or
obj.get("Name", ""))
if filt and filt not in display.lower():
continue
fname = (os.path.basename(proj["final_map"][i])
if i in proj["final_map"] else "---")
iid = tree.insert("", "end", iid=str(i), values=(display, fname))
if fname == "---":
tree.item(iid, tags=("gray",))
tree.tag_configure("gray", foreground=FG_GRAY)
notebook.bind("<<NotebookTabChanged>>", lambda *_: refresh_list())
# ═══════════════════════════════════════════════════════════════════════════
# BUILD
# ═══════════════════════════════════════════════════════════════════════════
def do_build():
ptype = TAB_ORDER[notebook.index(notebook.select())]
proj = projects[ptype]
scale = current_scale["v"]
tile_px = 16 * scale # e.g. 32 for 32x
if not proj["json_path"]:
messagebox.showwarning("No JSON",
f"Please load a {ptype.upper()} JSON first.")
return
cols, rows = CANVAS_TILES[ptype]
cw = cols * tile_px # canvas width in pixels
ch = rows * tile_px # canvas height in pixels
canvas_img = Image.new("RGBA", (cw, ch), (0, 0, 0, 0))
placed = 0
errors = 0
for k, fpath in proj["final_map"].items():
if not os.path.exists(fpath):
continue
try:
tile = load_tile(fpath, tile_px)
obj = proj["layout"][k]
# Convert JSON coord (always 16x pixel space) → grid slot → output pixel
json_x = int(obj.get("X", obj.get("x", 0)))
json_y = int(obj.get("Y", obj.get("y", 0)))
col = json_x // 16 # which grid column (0-based)
row = json_y // 16 # which grid row (0-based)
px = col * tile_px # output pixel X
py = row * tile_px # output pixel Y
# Use tile as its own alpha mask so transparent edges
# never overwrite adjacent tiles
canvas_img.paste(tile, (px, py), tile)
placed += 1
except Exception as e:
errors += 1
out_dir = os.path.dirname(proj["json_path"])
json_base = Path(proj["json_path"]).stem
out_path = os.path.join(out_dir, f"{json_base}.png")
canvas_img.save(out_path)
if ptype == "terrain":
canvas_img.resize((cw // 2, ch // 2), Image.NEAREST).save(
os.path.join(out_dir, "terrainMipMapLevel2.png"))
canvas_img.resize((cw // 4, ch // 4), Image.NEAREST).save(
os.path.join(out_dir, "terrainMipMapLevel3.png"))
res_label = {1: "16x", 2: "32x", 4: "64x"}[scale]
msg = (f"Resolution: {res_label}\n"
f"Canvas: {cw}×{ch}\n"
f"Tiles placed: {placed}")
if errors:
msg += f"\nErrors skipped: {errors}"
messagebox.showinfo("Build Complete", f"{msg}\n\nSaved to:\n{out_path}")
# ── BOTTOM BUILD BAR ───────────────────────────────────────────────────────
bottom = tk.Frame(root, bg=BG_DARK, height=70)
bottom.pack(side="bottom", fill="x", padx=10, pady=(0, 8))
bottom.pack_propagate(False)
tk.Button(bottom, text="BUILD ASSETS",
bg=BTN_GREEN, fg=FG_MAIN,
activebackground="#2E6B1A", activeforeground=FG_MAIN,
relief="flat", font=("Segoe UI", 12, "bold"),
command=do_build).pack(fill="both", expand=True)
root.mainloop()

BIN
Purple tex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

BIN
armour_boots.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

BIN
armour_chest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

BIN
armour_head.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

BIN
armour_leg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

BIN
brown thing 2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

BIN
brown thing 3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

BIN
brown thing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

BIN
firetex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

BIN
inventory_overlay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

1406
items.json Normal file

File diff suppressed because it is too large Load Diff

BIN
items.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
lava_flow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

BIN
quiver.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

BIN
ruby.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

BIN
spawn egg particle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

1868
terrain.json Normal file

File diff suppressed because it is too large Load Diff