import os
import tkinter as tk
from tkinter import filedialog, messagebox, ttk, Menu
class XbeShortcutGenerator:
def __init__(self, root):
self.root = root
self.root.title("Generador XBE Multi-Partición (Single File)")
self.root.geometry("750x650")
# --- Variables ---
self.source_xbe_path = tk.StringVar()
self.games_source_dir = tk.StringVar()
self.output_dir = tk.StringVar()
self.edit_path = tk.StringVar(value="Games\\")
# Diccionario para guardar el estado de los checkboxes
self.partitions_vars = {
"C:": tk.IntVar(),
"E:": tk.IntVar(),
"F:": tk.IntVar(),
"G:": tk.IntVar(),
"R:": tk.IntVar(),
"X:": tk.IntVar()
}
# Aquí guardaremos el "mapa" del archivo binario
self.detected_slots = {}
self.binary_data = None
self.create_widgets()
def create_widgets(self):
# 1. SELECCIÓN DE XBE MUESTRA
lf_source = ttk.LabelFrame(self.root, text="1. Plantilla XBE (Multi-Partición)", padding=10)
lf_source.pack(fill="x", padx=10, pady=5)
ttk.Entry(lf_source, textvariable=self.source_xbe_path, state="readonly").pack(side="left", fill="x", expand=True)
ttk.Button(lf_source, text="Cargar Plantilla", command=self.load_xbe_file).pack(side="right", padx=5)
# 2. CONFIGURACIÓN
lf_data = ttk.LabelFrame(self.root, text="2. Configuración de Rutas", padding=10)
lf_data.pack(fill="x", padx=10, pady=5)
# Info detectada
self.lbl_info = ttk.Label(lf_data, text="Estado: Esperando archivo...", foreground="gray")
self.lbl_info.grid(row=0, column=0, columnspan=6, sticky="w", pady=(0, 10))
ttk.Separator(lf_data, orient="horizontal").grid(row=1, column=0, columnspan=6, sticky="ew", pady=5)
# Checkboxes
ttk.Label(lf_data, text="Editar particiones:").grid(row=2, column=0, sticky="w", pady=5)
frame_checks = ttk.Frame(lf_data)
frame_checks.grid(row=3, column=0, columnspan=6, sticky="w")
col = 0
sorted_parts = sorted(self.partitions_vars.keys())
for part in sorted_parts:
cb = ttk.Checkbutton(frame_checks, text=part, variable=self.partitions_vars[part])
cb.grid(row=0, column=col, padx=10)
col += 1
# Ruta intermedia
ttk.Label(lf_data, text="Ruta intermedia (Carpeta contenedora):").grid(row=4, column=0, sticky="w", pady=(15,0))
ttk.Entry(lf_data, textvariable=self.edit_path).grid(row=5, column=0, columnspan=6, sticky="ew")
lf_data.columnconfigure(0, weight=1)
# 3. DIRECTORIOS Y GENERACIÓN
lf_dirs = ttk.LabelFrame(self.root, text="3. Generación", padding=10)
lf_dirs.pack(fill="x", padx=10, pady=5)
# Origen
ttk.Label(lf_dirs, text="Origen de juegos (Click Dcho para menú):").pack(anchor="w")
self.entry_games = ttk.Entry(lf_dirs, textvariable=self.games_source_dir)
self.entry_games.pack(fill="x", pady=(0, 10))
# Menú Contextual
self.context_menu = Menu(self.root, tearoff=0)
self.context_menu.add_command(label="📂 Seleccionar carpeta...", command=self.select_games_dir)
self.entry_games.bind("<Button-3>", self.show_context_menu) # Windows/Linux
self.entry_games.bind("<Button-2>", self.show_context_menu) # Mac
# Destino
ttk.Label(lf_dirs, text="Guardar nuevos XBE en:").pack(anchor="w")
frame_out = ttk.Frame(lf_dirs)
frame_out.pack(fill="x")
ttk.Entry(frame_out, textvariable=self.output_dir).pack(side="left", fill="x", expand=True)
ttk.Button(frame_out, text="...", width=3, command=self.select_output_dir).pack(side="right")
# BOTÓN
btn_create = ttk.Button(self.root, text="CREAR ATAJOS (SINGLE FILE)", command=self.generate_shortcuts)
btn_create.pack(fill="x", padx=20, pady=20, ipady=10)
# --- LÓGICA DE DETECCIÓN MULTI-SLOT ---
def load_xbe_file(self):
filename = filedialog.askopenfilename(filetypes=[("XBE", "*.xbe")])
if not filename: return
self.source_xbe_path.set(filename)
try:
with open(filename, "rb") as f:
self.binary_data = bytearray(f.read())
self.scan_for_slots()
except Exception as e:
messagebox.showerror("Error", f"Error al leer: {e}")
def scan_for_slots(self):
"""Busca TODAS las ocurrencias de 'default.xbe' y mapea sus particiones."""
self.detected_slots = {}
data = self.binary_data
search_term = b"default.xbe"
search_term_upper = b"DEFAULT.XBE"
cursor = 0
found_count = 0
while True:
# Buscar ocurrencia
idx = data.find(search_term, cursor)
if idx == -1:
idx = data.find(search_term_upper, cursor)
if idx == -1: break # No hay más
# Hemos encontrado un 'default.xbe' en 'idx'.
start = idx
while start > 0:
if data[start] == 0x00: # Tope nulo encontrado
start += 1
break
if (idx - start) > 260: break
start -= 1
full_path_bytes = data[start : idx + len(search_term)]
try:
path_str = full_path_bytes.decode('ascii', errors='ignore')
except:
path_str = ""
# Detectar partición (primeros 2 chars)
partition_key = path_str[:2].upper() # Ej: "E:", "F:"
# Calcular Padding disponible
end_padding = idx + len(search_term)
while end_padding < len(data) and data[end_padding] == 0x00:
end_padding += 1
max_len = (end_padding - start) - 1
# Guardar en diccionario si es una partición válida
if ":" in partition_key:
self.detected_slots[partition_key] = {
"start_offset": start,
"max_len": max_len,
"original_path": path_str
}
found_count += 1
if partition_key in self.partitions_vars:
self.partitions_vars[partition_key].set(1)
cursor = idx + 1
# Actualizar UI
found_parts = ", ".join(self.detected_slots.keys())
self.lbl_info.config(text=f"Detectadas {found_count} rutas en particiones: {found_parts}", foreground="green")
if found_count == 0:
messagebox.showwarning("Aviso", "No se encontraron rutas terminadas en 'default.xbe' en este archivo.")
# --- UTILIDADES ---
def show_context_menu(self, event):
try: self.context_menu.tk_popup(event.x_root, event.y_root)
finally: self.context_menu.grab_release()
def select_games_dir(self):
d = filedialog.askdirectory();
if d: self.games_source_dir.set(d)
def select_output_dir(self):
d = filedialog.askdirectory();
if d: self.output_dir.set(d)
# --- GENERACIÓN ---
def generate_shortcuts(self):
if not self.binary_data: return
if not self.detected_slots:
messagebox.showerror("Error", "No hay slots detectados en el archivo muestra.")
return
games_dir = self.games_source_dir.get()
out_dir = self.output_dir.get()
if not games_dir or not out_dir:
messagebox.showerror("Error", "Faltan rutas de origen/destino.")
return
mid_path = self.edit_path.get().strip()
if mid_path and not mid_path.endswith("\\"): mid_path += "\\"
if mid_path.startswith("\\"): mid_path = mid_path[1:]
try:
games = [d for d in os.listdir(games_dir) if os.path.isdir(os.path.join(games_dir, d))]
except Exception as e:
messagebox.showerror("Error", str(e))
return
active_partitions = [k for k, v in self.partitions_vars.items() if v.get() == 1]
errors = []
count = 0
for game in games:
# Creamos una copia fresca de los datos base para ESTE juego
new_file_data = bytearray(self.binary_data)
file_has_error = False
# Recorremos todas las particiones marcadas
for part in active_partitions:
if part not in self.detected_slots:
continue
slot = self.detected_slots[part]
drive_prefix = part + "\\"
new_path_str = f"{drive_prefix}{mid_path}{game}\\default.xbe"
new_path_bytes = new_path_str.encode('ascii')
if len(new_path_bytes) > slot['max_len']:
errors.append(f"[{game}] Ruta {part} demasiado larga. Max: {slot['max_len']}")
file_has_error = True
break
# Escribir en el buffer
offset = slot['start_offset']
available = slot['max_len'] + 1
for k in range(available):
new_file_data[offset + k] = 0x00
for k, b in enumerate(new_path_bytes):
new_file_data[offset + k] = b
if file_has_error:
continue
# --- CAMBIO APLICADO AQUÍ ---
# Nombre formato: "XBOX_" + nombreJuegoSinEspacios + ".xbe"
game_name_clean = game.replace(" ", "")
out_name = f"XBOX_{game_name_clean}.xbe"
# ----------------------------
out_path = os.path.join(out_dir, out_name)
try:
with open(out_path, "wb") as f:
f.write(new_file_data)
count += 1
except Exception as e:
errors.append(f"Error guardando {game}: {e}")
if errors:
msg = f"Creados: {count}\nErrores ({len(errors)}):\n" + "\n".join(errors[:5])
if len(errors) > 5: msg += "\n..."
messagebox.showwarning("Resultado", msg)
else:
messagebox.showinfo("Éxito", f"Se han creado {count} atajos correctamente.")
if __name__ == "__main__":
root = tk.Tk()
style = ttk.Style()
style.theme_use('clam')
app = XbeShortcutGenerator(root)
root.mainloop()