From 427aac1c32ef40726ce637517e1ac3df3be4fe29 Mon Sep 17 00:00:00 2001 From: sanguchito <97652967+Martuccii@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:14:06 -0300 Subject: [PATCH] Modtools --- Modtools/pck_tool.py | 222 +++++++++++++++++++++++++++++++++++++++++++ Modtools/readme.md | 112 ++++++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 Modtools/pck_tool.py create mode 100644 Modtools/readme.md diff --git a/Modtools/pck_tool.py b/Modtools/pck_tool.py new file mode 100644 index 000000000..773db4167 --- /dev/null +++ b/Modtools/pck_tool.py @@ -0,0 +1,222 @@ +import os +import re +import tkinter as tk +from tkinter import filedialog, messagebox +import customtkinter as ctk +from tkinterdnd2 import DND_FILES, TkinterDnD + +PNG_HEADER = b"\x89PNG\r\n\x1a\n" + +LANGUAGES = { + "English": { + "title": "PCK Skin Tool", + "drop": "DRAG & DROP PCK FILES HERE", + "extract_folder": "Extract Folder", + "create_pck": "Create PCK", + "language": "Language", + "theme": "Theme:", + "logs": "Activity Log", + "success_extract": "Extraction completed", + "success_create": "PCK created successfully", + "error_no_png": "No PNG files found", + "error_no_png_in_pck": "No PNG inside PCK" + }, + "Español": { + "title": "Herramienta PCK", + "drop": "ARRASTRA ARCHIVOS .PCK AQUÍ", + "extract_folder": "Extraer Carpeta", + "create_pck": "Crear PCK", + "language": "Idioma", + "theme": "Tema:", + "logs": "Registro de Actividad", + "success_extract": "Extracción completada", + "success_create": "PCK creado correctamente", + "error_no_png": "No se encontraron PNG", + "error_no_png_in_pck": "Sin PNG dentro del PCK" + } +} + +ctk.set_appearance_mode("dark") +ctk.set_default_color_theme("blue") + +class PCKTool(TkinterDnD.Tk): + def __init__(self): + super().__init__() + + + self.language = "English" + self.translations = LANGUAGES[self.language] + + current_mode = 1 if ctk.get_appearance_mode() == "Dark" else 0 + bg_color = ctk.ThemeManager.theme["CTk"]["fg_color"][current_mode] + self.config(background=bg_color) + + self.title(self.translations["title"]) + self.geometry("850x570") + + # Grid layout + self.grid_columnconfigure(1, weight=1) + self.grid_rowconfigure(0, weight=1) + + # --- SIDEBAR --- + self.sidebar = ctk.CTkFrame( + self, + width=220, + corner_radius=0, + fg_color=("#EDEDED", "#1A1A1A") + ) + self.sidebar.grid(row=0, column=0, sticky="nsew") + self.sidebar.grid_rowconfigure(6, weight=1) + + self.logo_label = ctk.CTkLabel( + self.sidebar, + text="PCK EXTRACTOR", + font=ctk.CTkFont(size=20, weight="bold"), + anchor="w" + ) + self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 30), sticky="w") + + self.lang_label = ctk.CTkLabel(self.sidebar, text=self.translations["language"], anchor="w") + self.lang_label.grid(row=1, column=0, padx=20, pady=(10, 0), sticky="w") + + self.lang_menu = ctk.CTkOptionMenu(self.sidebar, values=list(LANGUAGES.keys()), command=self.change_language) + self.lang_menu.set(self.language) + self.lang_menu.grid(row=2, column=0, padx=20, pady=10, sticky="w") + + # self.theme_label = ctk.CTkLabel(self.sidebar, text=self.translations["theme"], anchor="w") + # self.theme_label.grid(row=3, column=0, padx=20, pady=(20, 0), sticky="w") + + # self.appearance_mode_menu = ctk.CTkOptionMenu(self.sidebar, values=["Dark", "Light"], command=self.change_appearance_mode) + # self.appearance_mode_menu.grid(row=4, column=0, padx=20, pady=10, sticky="w") + + # --- MAIN CONTENT --- + self.main_container = ctk.CTkFrame(self, fg_color="transparent") + self.main_container.grid(row=0, column=1, padx=20, pady=20, sticky="nsew") + self.main_container.grid_columnconfigure(0, weight=1) + self.main_container.grid_rowconfigure(3, weight=1) + + # Drop Zone + self.drop_frame = ctk.CTkFrame( + self.main_container, + border_width=2, + border_color="#3B8ED0", + fg_color=("white", "#2B2B2B") + ) + self.drop_frame.grid(row=0, column=0, sticky="nsew", pady=(0, 20), ipady=50) + self.drop_frame.grid_columnconfigure(0, weight=1) + self.drop_frame.grid_rowconfigure(0, weight=1) + + self.drop_label = ctk.CTkLabel(self.drop_frame, text=self.translations["drop"], + font=ctk.CTkFont(size=16, weight="bold")) + self.drop_label.grid(row=0, column=0) + + self.drop_frame.drop_target_register(DND_FILES) + self.drop_frame.dnd_bind("<>", self.drop_file) + + # Action Buttons + self.button_frame = ctk.CTkFrame(self.main_container, fg_color="transparent") + self.button_frame.grid(row=1, column=0, sticky="ew") + self.button_frame.grid_columnconfigure((0, 1), weight=1) + + self.extract_button = ctk.CTkButton(self.button_frame, text=self.translations["extract_folder"], + height=45, fg_color="#1f538d", command=self.extract_folder) + self.extract_button.grid(row=0, column=0, padx=5, pady=10, sticky="ew") + + self.create_button = ctk.CTkButton(self.button_frame, text=self.translations["create_pck"], + height=45, fg_color="#2b8a3e", hover_color="#1e5f2b", + command=self.create_pck) + self.create_button.grid(row=0, column=1, padx=5, pady=10, sticky="ew") + + # Logs + self.log_label = ctk.CTkLabel(self.main_container, text=self.translations["logs"], font=ctk.CTkFont(weight="bold")) + self.log_label.grid(row=2, column=0, sticky="w", pady=(10, 0)) + + self.textbox = ctk.CTkTextbox(self.main_container, corner_radius=10, border_width=1) + self.textbox.grid(row=3, column=0, sticky="nsew", pady=(5, 0)) + + self.log("System initialized.") + + def log(self, message): + self.textbox.insert("end", f"[*] {message}\n") + self.textbox.see("end") + + def change_appearance_mode(self, new_mode: str): + ctk.set_appearance_mode(new_mode) + current_mode = 1 if new_mode == "Dark" else 0 + bg_color = ctk.ThemeManager.theme["CTk"]["fg_color"][current_mode] + self.config(background=bg_color) + + def change_language(self, choice): + self.language = choice + self.translations = LANGUAGES[self.language] + self.refresh_ui() + + def refresh_ui(self): + self.title(self.translations["title"]) + self.lang_label.configure(text=self.translations["language"]) + self.theme_label.configure(text=self.translations["theme"]) + self.drop_label.configure(text=self.translations["drop"]) + self.extract_button.configure(text=self.translations["extract_folder"]) + self.create_button.configure(text=self.translations["create_pck"]) + self.log_label.configure(text=self.translations["logs"]) + + def extract_pck(self, filepath): + try: + filename = os.path.basename(filepath) + with open(filepath, "rb") as f: + data = f.read() + positions = [m.start() for m in re.finditer(PNG_HEADER, data)] + if not positions: + self.log(f"Error: No PNGs in {filename}") + return + output_dir = os.path.splitext(filepath)[0] + "_extracted" + os.makedirs(output_dir, exist_ok=True) + for i, pos in enumerate(positions): + next_pos = positions[i+1] if i+1 < len(positions) else len(data) + chunk = data[pos:next_pos] + with open(os.path.join(output_dir, f"skin_{i+1:03d}.png"), "wb") as out: + out.write(chunk) + self.log(f"Success: {filename} -> {len(positions)} skins extracted.") + except Exception as e: + self.log(f"Critical Error: {str(e)}") + + def drop_file(self, event): + raw_data = event.data + if raw_data.startswith('{') and raw_data.endswith('}'): + paths = [raw_data[1:-1]] + else: + paths = self.tk.splitlist(raw_data) + for path in paths: + if path.lower().endswith(".pck"): + self.extract_pck(path) + messagebox.showinfo("Process", self.translations["success_extract"]) + + def extract_folder(self): + folder = filedialog.askdirectory() + if not folder: return + pck_files = [f for f in os.listdir(folder) if f.lower().endswith(".pck")] + for file in pck_files: + self.extract_pck(os.path.join(folder, file)) + messagebox.showinfo("OK", self.translations["success_extract"]) + + def create_pck(self): + folder = filedialog.askdirectory() + if not folder: return + pngs = sorted([f for f in os.listdir(folder) if f.lower().endswith(".png")]) + if not pngs: + messagebox.showerror("Error", self.translations["error_no_png"]) + return + output_path = os.path.join(folder, "custom_skins.pck") + try: + with open(output_path, "wb") as out: + for png in pngs: + with open(os.path.join(folder, png), "rb") as f: + out.write(f.read()) + self.log(f"Created PCK: {output_path} ({len(pngs)} images)") + messagebox.showinfo("OK", self.translations["success_create"]) + except Exception as e: + self.log(f"Error: {str(e)}") + +if __name__ == "__main__": + app = PCKTool() + app.mainloop() \ No newline at end of file diff --git a/Modtools/readme.md b/Modtools/readme.md new file mode 100644 index 000000000..5577a3891 --- /dev/null +++ b/Modtools/readme.md @@ -0,0 +1,112 @@ +# PCK Decompiler + +Technical Documentation and Project Specifications + +--- + +## Overview + +**PCK Decompiler** is an open-source Python tool designed to extract and rebuild `.pck` files containing image assets (PNG) used in game UI systems. + +--- + +## Architecture + +The tool is composed of: + +- `pck_tool.py` → Main application +- Graphical Interface built with `CustomTkinter` +- Drag & Drop support via `tkinterdnd2` +- Image handling via `Pillow` + +It supports: + +- Drag & Drop `.pck` files +- Folder-wide decompilation +- PNG injection +- Language switching (UI localization) + +--- + +## Installation + +### 1️⃣ Requirements + +- Python 3.9+ +- Visual Studio Code + +--- + +### 2️⃣ Install Dependencies (Recommended via VS Code Terminal) + +Open the project folder in **Visual Studio Code**. + +Then open the integrated terminal: + +Terminal → New Terminal + +Run the following commands: + +python -m venv venv + +Activate the virtual environment: + +Windows: + +venv\\Scripts\\activate + +Install required packages: + +pip install --upgrade pip +pip install customtkinter pillow tkinterdnd2 + +--- + +## Running the Tool (via VS Code Terminal) + +With the virtual environment activated inside VS Code terminal: + +python pck_tool.py + +The GUI will launch automatically. + +--- + +## Features + +### Drag & Drop + +Drop `.pck` files directly into the application window to extract contents. + +### Folder Decompilation + +Select a folder and the tool will process all `.pck` files inside it. + +### PNG Injection + +Import custom `.png` files and rebuild the `.pck` structure. + +### Language Selection + +Switch UI language dynamically from the interface. + +--- + +## Repository Integration + +This tool is designed to: + +- Live inside another repository +- Be submitted as a Pull Request +- Not require independent directory generation +- Not auto-create standalone project folders + +It behaves as an internal utility module. + +--- + +## Known Limitations + +- Supports PNG-based PCK structures +- Does not reverse proprietary compression (if present) +- Assumes consistent internal file structure