initial commit
Functioning build
388
MLCE Converter.py
Normal 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
|
After Width: | Height: | Size: 125 B |
BIN
armour_boots.png
Normal file
|
After Width: | Height: | Size: 157 B |
BIN
armour_chest.png
Normal file
|
After Width: | Height: | Size: 162 B |
BIN
armour_head.png
Normal file
|
After Width: | Height: | Size: 163 B |
BIN
armour_leg.png
Normal file
|
After Width: | Height: | Size: 155 B |
BIN
brown thing 2.png
Normal file
|
After Width: | Height: | Size: 175 B |
BIN
brown thing 3.png
Normal file
|
After Width: | Height: | Size: 187 B |
BIN
brown thing.png
Normal file
|
After Width: | Height: | Size: 164 B |
BIN
firetex.png
Normal file
|
After Width: | Height: | Size: 173 B |
BIN
inventory_overlay.png
Normal file
|
After Width: | Height: | Size: 146 B |
1406
items.json
Normal file
BIN
lava_flow.png
Normal file
|
After Width: | Height: | Size: 287 B |
BIN
quiver.png
Normal file
|
After Width: | Height: | Size: 258 B |
BIN
spawn egg particle.png
Normal file
|
After Width: | Height: | Size: 334 B |