segon commit
This commit is contained in:
434
tools/font_gen/font_gen.py
Normal file
434
tools/font_gen/font_gen.py
Normal file
@@ -0,0 +1,434 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generador de fuentes bitmap para Projecte 2026.
|
||||
|
||||
Convierte un archivo .ttf (o un GIF existente) en un GIF indexado + fichero .fnt
|
||||
compatibles con el sistema de texto del juego.
|
||||
|
||||
Dependencias: pip install Pillow
|
||||
|
||||
Uso (desde TTF):
|
||||
python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont
|
||||
python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont --dir data/font --box-width 8
|
||||
|
||||
Uso (desde GIF existente con cuadrícula):
|
||||
python3 font_gen.py --gif myfont.gif --output myfont --columns 16 --cell-spacing 1
|
||||
|
||||
Notas:
|
||||
- Para fuentes bitmap (pixel fonts) en TTF, usa el tamaño exacto del bitmap strike.
|
||||
- Los glifos se almacenan como índice de paleta 1 (blanco) sobre fondo transparente (índice 0).
|
||||
- Esto es compatible con SurfaceSprite::render(1, color) del motor del juego.
|
||||
- Los caracteres no incluidos en la fuente aparecerán como celdas vacías en el GIF.
|
||||
- El modo --gif acepta un GIF cuya cuadrícula siga el orden de ALL_CHARS.
|
||||
Celdas vacías (todos los píxeles = índice 0) se marcan como no soportadas.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from math import ceil
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
except ImportError:
|
||||
print("Error: Pillow no está instalado. Ejecuta: pip install Pillow", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Conjunto de caracteres en el mismo orden que los ficheros .fnt del juego.
|
||||
# ASCII 32-126 primero, luego extensiones para castellano, catalán y valenciano.
|
||||
_ASCII_CHARS = [chr(i) for i in range(32, 127)] # 95 chars: espacio … ~
|
||||
_EXTENDED_CHARS = list("ÀÁÈÉÌÍÒÓÙÚÑÇàáèéìíòóùúñç¡¿«»·") # 29 chars ES/CA/VA
|
||||
ALL_CHARS = _ASCII_CHARS + _EXTENDED_CHARS # 124 total
|
||||
|
||||
# Caracteres de fallback para TTFs sin soporte de acentos/especiales.
|
||||
# Si el TTF no tiene el char, se dibuja el fallback en su celda del bitmap.
|
||||
# El .fnt sigue registrando el codepoint original → texto localizado funciona.
|
||||
CHAR_FALLBACKS: dict[str, str] = {
|
||||
"À": "A", "Á": "A", "È": "E", "É": "E",
|
||||
"Ì": "I", "Í": "I", "Ò": "O", "Ó": "O",
|
||||
"Ù": "U", "Ú": "U", "Ñ": "N", "Ç": "C",
|
||||
"à": "a", "á": "a", "è": "e", "é": "e",
|
||||
"ì": "i", "í": "i", "ò": "o", "ó": "o",
|
||||
"ù": "u", "ú": "u", "ñ": "n", "ç": "c",
|
||||
"¡": "!", "¿": "?", "«": "<", "»": ">", "·": ".",
|
||||
}
|
||||
|
||||
|
||||
def _write_fnt(
|
||||
output_fnt: str,
|
||||
output_name: str,
|
||||
source_name: str,
|
||||
box_width: int,
|
||||
box_height: int,
|
||||
columns: int,
|
||||
cell_spacing: int,
|
||||
row_spacing: int,
|
||||
chars: list[str],
|
||||
char_widths: dict[str, int],
|
||||
) -> None:
|
||||
"""Escribe el fichero .fnt."""
|
||||
with open(output_fnt, "w", encoding="utf-8") as f:
|
||||
f.write(f"# Font: {output_name} — generado desde {source_name}\n")
|
||||
f.write(f"# Generado con tools/font_gen/font_gen.py\n\n")
|
||||
f.write(f"box_width {box_width}\n")
|
||||
f.write(f"box_height {box_height}\n")
|
||||
f.write(f"columns {columns}\n")
|
||||
if cell_spacing:
|
||||
f.write(f"cell_spacing {cell_spacing}\n")
|
||||
if row_spacing and row_spacing != cell_spacing:
|
||||
f.write(f"row_spacing {row_spacing}\n")
|
||||
f.write("\n# codepoint_decimal ancho_visual\n")
|
||||
for ch in chars:
|
||||
cp = ord(ch)
|
||||
w = char_widths[ch]
|
||||
name = ch if ch.isprintable() and not ch.isspace() else f"U+{cp:04X}"
|
||||
f.write(f"{cp} {w} # {name}\n")
|
||||
|
||||
|
||||
def render_gif_font(
|
||||
gif_path: str,
|
||||
output_name: str,
|
||||
output_dir: str,
|
||||
columns: int,
|
||||
box_width: int,
|
||||
box_height: int,
|
||||
cell_spacing: int,
|
||||
row_spacing: int,
|
||||
) -> None:
|
||||
"""Genera el .fnt a partir de un GIF de fuente existente con cuadrícula.
|
||||
|
||||
Fórmula de posición de cada celda (col, row):
|
||||
x0 = col * (box_width + cell_spacing) + cell_spacing
|
||||
y0 = row * (box_height + row_spacing) + cell_spacing
|
||||
"""
|
||||
|
||||
if not os.path.isfile(gif_path):
|
||||
print(f"Error: No se encuentra el archivo GIF: {gif_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
output_gif = os.path.join(output_dir, f"{output_name}.gif")
|
||||
output_fnt = os.path.join(output_dir, f"{output_name}.fnt")
|
||||
|
||||
img = Image.open(gif_path)
|
||||
if img.mode != "P":
|
||||
img = img.convert("P")
|
||||
img_w, img_h = img.size
|
||||
pixels = img.load()
|
||||
|
||||
num_rows = ceil(len(ALL_CHARS) / columns)
|
||||
stride_x = box_width + cell_spacing
|
||||
stride_y = box_height + row_spacing
|
||||
|
||||
print(f"GIF fuente: {os.path.basename(gif_path)} ({img_w}×{img_h} px)")
|
||||
print(f"Caja : {box_width}×{box_height} px | {columns} cols, {num_rows} filas | spacing x={cell_spacing} y={row_spacing}")
|
||||
|
||||
char_widths: dict[str, int] = {}
|
||||
supported_chars: list[str] = []
|
||||
skipped: list[str] = []
|
||||
|
||||
for i, ch in enumerate(ALL_CHARS):
|
||||
col = i % columns
|
||||
row = i // columns
|
||||
x0 = col * stride_x + cell_spacing
|
||||
y0 = row * stride_y + cell_spacing
|
||||
|
||||
# Comprobar límites (el GIF puede tener más o menos filas que ALL_CHARS)
|
||||
if y0 + box_height > img_h or x0 + box_width > img_w:
|
||||
skipped.append(ch)
|
||||
continue
|
||||
|
||||
# Celda vacía (todos los píxeles = índice 0)
|
||||
if all(pixels[x0 + px, y0 + py] == 0 for py in range(box_height) for px in range(box_width)):
|
||||
if ch.isspace():
|
||||
# El espacio no tiene píxeles visibles: asignar ancho por defecto
|
||||
char_widths[ch] = max(1, box_width // 2)
|
||||
supported_chars.append(ch)
|
||||
else:
|
||||
skipped.append(ch)
|
||||
continue
|
||||
|
||||
# Medir ancho visual: última columna con algún píxel no-fondo
|
||||
pixel_width = 0
|
||||
for px in range(box_width - 1, -1, -1):
|
||||
if any(pixels[x0 + px, y0 + py] != 0 for py in range(box_height)):
|
||||
pixel_width = px + 1
|
||||
break
|
||||
|
||||
char_widths[ch] = max(1, pixel_width)
|
||||
supported_chars.append(ch)
|
||||
|
||||
if skipped:
|
||||
names = "".join(c if c.isprintable() and not c.isspace() else f"[U+{ord(c):04X}]" for c in skipped)
|
||||
print(f"Omitidos : {len(skipped)} chars vacíos/fuera de rango: {names}")
|
||||
print(f"Soportados: {len(supported_chars)} caracteres")
|
||||
|
||||
# Copiar el GIF al directorio de salida
|
||||
shutil.copy2(gif_path, output_gif)
|
||||
print(f"GIF : {output_gif}")
|
||||
|
||||
_write_fnt(output_fnt, output_name, os.path.basename(gif_path),
|
||||
box_width, box_height, columns, cell_spacing, row_spacing, supported_chars, char_widths)
|
||||
print(f"FNT : {output_fnt}")
|
||||
|
||||
|
||||
def render_font(
|
||||
ttf_path: str,
|
||||
size: int,
|
||||
output_name: str,
|
||||
output_dir: str,
|
||||
columns: int,
|
||||
box_width_override: int | None,
|
||||
box_height_override: int | None,
|
||||
) -> None:
|
||||
"""Genera el GIF indexado y el .fnt a partir de un archivo .ttf."""
|
||||
|
||||
if not os.path.isfile(ttf_path):
|
||||
print(f"Error: No se encuentra el archivo TTF: {ttf_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
output_gif = os.path.join(output_dir, f"{output_name}.gif")
|
||||
output_fnt = os.path.join(output_dir, f"{output_name}.fnt")
|
||||
|
||||
# --- Cargar fuente ---
|
||||
try:
|
||||
font = ImageFont.truetype(ttf_path, size)
|
||||
except OSError as e:
|
||||
print(f"Error al cargar la fuente: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Calcular dimensiones de la caja ---
|
||||
# box_height = línea completa: ascent (sobre la línea base) + descent (bajo ella)
|
||||
ascent, descent = font.getmetrics()
|
||||
box_height = box_height_override if box_height_override is not None else (ascent + abs(descent))
|
||||
|
||||
# Calcular y_offset antes de la clasificación (necesario para detectar .notdef)
|
||||
cap_tops = [font.getbbox(ch)[1] for ch in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" if font.getbbox(ch)]
|
||||
y_offset = -round(sum(cap_tops) / len(cap_tops)) if cap_tops else 0
|
||||
|
||||
# Detectar el glifo .notdef: algunos TTF devuelven un glifo sustituto (con
|
||||
# advance > 0 y bbox válido) para chars que no tienen en su cmap. Esto provoca
|
||||
# falsos positivos en el test getlength >= 1. Se renderiza U+FFFE (garantizado
|
||||
# ausente en cualquier fuente de uso normal) y se guardan sus píxeles como
|
||||
# referencia. Cualquier char con píxeles idénticos se considera no soportado.
|
||||
_tmp_w = box_width_override or 32
|
||||
_nd_bbox = font.getbbox(chr(0xFFFE))
|
||||
if _nd_bbox:
|
||||
_nd_img = Image.new("RGBA", (_tmp_w, box_height), (0, 0, 0, 0))
|
||||
ImageDraw.Draw(_nd_img).text((-_nd_bbox[0], y_offset), chr(0xFFFE),
|
||||
font=font, fill=(255, 255, 255, 255))
|
||||
_notdef_bytes = _nd_img.tobytes()
|
||||
else:
|
||||
_notdef_bytes = None
|
||||
|
||||
def _is_notdef(ch: str) -> bool:
|
||||
"""True si el char renderiza el glifo .notdef en lugar de un glifo real."""
|
||||
if _notdef_bytes is None:
|
||||
return False
|
||||
bbox = font.getbbox(ch)
|
||||
if not bbox:
|
||||
return True
|
||||
img = Image.new("RGBA", (_tmp_w, box_height), (0, 0, 0, 0))
|
||||
ImageDraw.Draw(img).text((-bbox[0], y_offset), ch, font=font, fill=(255, 255, 255, 255))
|
||||
return img.tobytes() == _notdef_bytes
|
||||
|
||||
# Clasificar chars: soportados nativamente, con fallback, o sin soporte.
|
||||
# Un char tiene soporte nativo si su advance >= 1 Y no renderiza .notdef.
|
||||
# Si no, se busca en CHAR_FALLBACKS. Sin soporte y sin fallback, se omite.
|
||||
chars_to_render: list[str] = []
|
||||
char_render_as: dict[str, str] = {} # char → qué char dibujar realmente
|
||||
truly_skipped: list[str] = []
|
||||
|
||||
for ch in ALL_CHARS:
|
||||
if ch.isspace():
|
||||
chars_to_render.append(ch)
|
||||
char_render_as[ch] = ch
|
||||
elif font.getlength(ch) >= 1.0 and not _is_notdef(ch):
|
||||
chars_to_render.append(ch)
|
||||
char_render_as[ch] = ch
|
||||
elif ch in CHAR_FALLBACKS and font.getlength(CHAR_FALLBACKS[ch]) >= 1.0:
|
||||
chars_to_render.append(ch)
|
||||
char_render_as[ch] = CHAR_FALLBACKS[ch]
|
||||
else:
|
||||
truly_skipped.append(ch)
|
||||
|
||||
if truly_skipped:
|
||||
print(f"Omitidos : {len(truly_skipped)} chars sin soporte ni fallback: {''.join(truly_skipped)}")
|
||||
fallback_used = [ch for ch, r in char_render_as.items() if r != ch]
|
||||
if fallback_used:
|
||||
print(f"Fallback : {len(fallback_used)} chars con fallback: " +
|
||||
" ".join(f"{ch}→{char_render_as[ch]}" for ch in fallback_used))
|
||||
|
||||
# Medir advance width tipográfico: solo se usa para calcular box_width del canvas
|
||||
# cuando el usuario no lo especifica. El ancho real del .fnt se mide desde píxeles.
|
||||
char_widths: dict[str, int] = {}
|
||||
for ch in chars_to_render:
|
||||
render_ch = char_render_as[ch]
|
||||
char_widths[ch] = max(1, int(font.getlength(render_ch)))
|
||||
|
||||
# box_width: anchura de cada celda en el bitmap.
|
||||
# Si el usuario la especifica, se usa tal cual. Si no, se calcula como el
|
||||
# advance máximo (sin padding extra, ya que getlength incluye el espaciado).
|
||||
if box_width_override is not None:
|
||||
box_width = box_width_override
|
||||
else:
|
||||
box_width = max(char_widths.values())
|
||||
|
||||
# --- Calcular dimensiones del bitmap completo ---
|
||||
rows = ceil(len(chars_to_render) / columns)
|
||||
img_width = columns * box_width
|
||||
img_height = rows * box_height
|
||||
|
||||
print(f"Fuente : {os.path.basename(ttf_path)}, size={size}")
|
||||
print(f"Caja : {box_width}×{box_height} px | {columns} columnas, {rows} filas")
|
||||
print(f"Bitmap : {img_width}×{img_height} px | {len(chars_to_render)} caracteres")
|
||||
|
||||
# --- Buffer de píxeles: 0 = fondo transparente, 1 = glifo ---
|
||||
pixels = bytearray(img_width * img_height)
|
||||
|
||||
# --- Renderizar cada carácter ---
|
||||
for i, ch in enumerate(chars_to_render):
|
||||
col = i % columns
|
||||
row = i // columns
|
||||
cell_x = col * box_width
|
||||
cell_y = row * box_height
|
||||
|
||||
draw_ch = char_render_as[ch] # char que realmente se dibuja (puede ser fallback)
|
||||
bbox = font.getbbox(draw_ch)
|
||||
if not bbox:
|
||||
# Sin glifos visibles (ej. espacio): celda vacía, correcto.
|
||||
continue
|
||||
|
||||
# Renderizar a imagen RGBA con fondo transparente, texto blanco.
|
||||
#
|
||||
# POSICIONAMIENTO VERTICAL: se usa y=0 para respetar la posición del
|
||||
# carácter relativa a la línea base del em-box. draw.text((x, 0), ch)
|
||||
# coloca la parte superior del em-box en y=0, de modo que la línea base
|
||||
# queda en y=ascent y los signos de puntuación (que están cerca de la
|
||||
# línea base) aparecen en la parte inferior de la celda, como es correcto.
|
||||
#
|
||||
# POSICIONAMIENTO HORIZONTAL: -bbox[0] alinea el borde izquierdo del
|
||||
# glifo al inicio de la celda, compensando el bearing izquierdo.
|
||||
char_img = Image.new("RGBA", (box_width, box_height), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(char_img)
|
||||
draw.text((-bbox[0], y_offset), draw_ch, font=font, fill=(255, 255, 255, 255))
|
||||
|
||||
# Umbralizar alpha y volcar al buffer de índices
|
||||
char_bytes = char_img.tobytes()
|
||||
for py in range(box_height):
|
||||
for px in range(box_width):
|
||||
src = (py * box_width + px) * 4
|
||||
if char_bytes[src + 3] > 128:
|
||||
pixels[(cell_y + py) * img_width + (cell_x + px)] = 1
|
||||
|
||||
# Medir ancho visual real: última columna con algún píxel opaco.
|
||||
# Reemplaza el advance tipográfico de getlength() que incluye side-bearings.
|
||||
for px in range(box_width - 1, -1, -1):
|
||||
if any(char_bytes[(py * box_width + px) * 4 + 3] > 128 for py in range(box_height)):
|
||||
char_widths[ch] = px + 1
|
||||
break
|
||||
|
||||
# --- Crear imagen P (paleta indexada 8 bits) ---
|
||||
img = Image.frombytes("P", (img_width, img_height), bytes(pixels))
|
||||
|
||||
# Paleta mínima: índice 0 = negro (transparente), índice 1 = blanco (glifo)
|
||||
palette = [0] * 768
|
||||
palette[3] = palette[4] = palette[5] = 255 # índice 1 → blanco
|
||||
img.putpalette(palette)
|
||||
|
||||
# Guardar GIF: índice 0 = fondo, índice 1 = glifo.
|
||||
# Pillow sin transparency escribe GIF87a. Luego magick reduce la paleta a
|
||||
# 2 colores (lzw_min=2) para compatibilidad exacta con el parser gif.cpp.
|
||||
img.save(output_gif, optimize=False)
|
||||
try:
|
||||
import subprocess
|
||||
subprocess.run(
|
||||
["magick", output_gif, "-colors", "2", f"GIF87:{output_gif}"],
|
||||
check=True, capture_output=True,
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
pass # si magick no está disponible se queda como GIF87a de 256 colores
|
||||
print(f"GIF : {output_gif}")
|
||||
|
||||
_write_fnt(output_fnt, output_name, f"{os.path.basename(ttf_path)} size {size}",
|
||||
box_width, box_height, columns, 0, 0, chars_to_render, char_widths)
|
||||
print(f"FNT : {output_fnt}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Directorio del proyecto: dos niveles arriba de este script (tools/font_gen/)
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_dir = os.path.dirname(os.path.dirname(script_dir))
|
||||
default_dir = os.path.join(project_dir, "data", "font")
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Genera fuentes bitmap (.gif + .fnt) desde un .ttf o un GIF existente.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Ejemplos (desde TTF):
|
||||
python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont
|
||||
python3 font_gen.py --ttf myfont.ttf --size 8 --output myfont --box-width 8
|
||||
python3 font_gen.py --ttf myfont.ttf --size 16 --output myfont --dir data/font
|
||||
|
||||
Ejemplos (desde GIF con cuadrícula):
|
||||
python3 font_gen.py --gif myfont.gif --output myfont --columns 16 --box-width 10 --box-height 7 --cell-spacing 1
|
||||
python3 font_gen.py --gif myfont.gif --output myfont --columns 16 --box-width 10 --box-height 7 --cell-spacing 1 --row-spacing 4
|
||||
|
||||
Notas:
|
||||
- Para pixel fonts (.ttf bitmap), usa el tamaño exacto del bitmap strike.
|
||||
- El GIF resultante usa índice 1 = glifo, índice 0 = transparente.
|
||||
- Se procesan 124 caracteres: ASCII 32-126 + extensiones ES/CA/VA.
|
||||
- En modo --gif, --box-width y --box-height son obligatorios.
|
||||
- Las celdas completamente vacías (índice 0) se marcan como no soportadas.
|
||||
- cell_spacing = spacing horizontal entre columnas (y borde izquierdo/superior).
|
||||
- row_spacing = spacing vertical entre filas (si difiere de cell_spacing).
|
||||
""",
|
||||
)
|
||||
|
||||
# Fuente: TTF o GIF (mutuamente excluyentes)
|
||||
source_group = parser.add_mutually_exclusive_group(required=True)
|
||||
source_group.add_argument("--ttf", help="Ruta al archivo .ttf")
|
||||
source_group.add_argument("--gif", help="Ruta a un GIF de fuente existente con cuadrícula")
|
||||
|
||||
parser.add_argument("--size", type=int, help="Tamaño en píxeles (solo con --ttf)")
|
||||
parser.add_argument("--output", required=True, help="Nombre base de salida (sin extensión)")
|
||||
parser.add_argument("--dir", default=default_dir, help="Directorio de salida (default: data/font/)")
|
||||
parser.add_argument("--columns", default=15, type=int, help="Columnas en el bitmap (default: 15)")
|
||||
parser.add_argument("--box-width", default=None, type=int, help="Anchura fija de celda en px (default: auto)")
|
||||
parser.add_argument("--box-height", default=None, type=int, help="Altura fija de celda en px (default: auto)")
|
||||
parser.add_argument("--cell-spacing", default=0, type=int, help="Píxeles de separación entre columnas, y borde (default: 0)")
|
||||
parser.add_argument("--row-spacing", default=None, type=int, help="Píxeles de separación entre filas (default: igual a --cell-spacing)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.gif:
|
||||
if args.box_width is None or args.box_height is None:
|
||||
parser.error("--box-width y --box-height son obligatorios con --gif")
|
||||
row_sp = args.row_spacing if args.row_spacing is not None else args.cell_spacing
|
||||
render_gif_font(
|
||||
gif_path = args.gif,
|
||||
output_name = args.output,
|
||||
output_dir = args.dir,
|
||||
columns = args.columns,
|
||||
box_width = args.box_width,
|
||||
box_height = args.box_height,
|
||||
cell_spacing = args.cell_spacing,
|
||||
row_spacing = row_sp,
|
||||
)
|
||||
else:
|
||||
if args.size is None:
|
||||
parser.error("--size es obligatorio cuando se usa --ttf")
|
||||
render_font(
|
||||
ttf_path = args.ttf,
|
||||
size = args.size,
|
||||
output_name = args.output,
|
||||
output_dir = args.dir,
|
||||
columns = args.columns,
|
||||
box_width_override = args.box_width,
|
||||
box_height_override = args.box_height,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
53
tools/pack_resources/Makefile
Normal file
53
tools/pack_resources/Makefile
Normal file
@@ -0,0 +1,53 @@
|
||||
# Makefile for pack_resources tool
|
||||
|
||||
# Compiler
|
||||
CXX := g++
|
||||
CXXFLAGS := -std=c++20 -Wall -Wextra -O2
|
||||
|
||||
# Directories
|
||||
TOOL_DIR := .
|
||||
SOURCE_DIR := ../../source/core/resources
|
||||
|
||||
# Source files
|
||||
SOURCES := pack_resources.cpp \
|
||||
$(SOURCE_DIR)/resource_pack.cpp
|
||||
|
||||
# Output
|
||||
TARGET := pack_resources
|
||||
|
||||
# Platform-specific executable extension
|
||||
ifeq ($(OS),Windows_NT)
|
||||
TARGET := $(TARGET).exe
|
||||
endif
|
||||
|
||||
# Default target
|
||||
all: $(TARGET)
|
||||
|
||||
# Build the tool
|
||||
$(TARGET): $(SOURCES)
|
||||
@echo "Building pack_resources tool..."
|
||||
$(CXX) $(CXXFLAGS) $(SOURCES) -o $(TARGET)
|
||||
@echo "Build complete: $(TARGET)"
|
||||
|
||||
# Test: create a test pack
|
||||
test: $(TARGET)
|
||||
@echo "Creating test pack..."
|
||||
./$(TARGET) ../../data test_resources.pack
|
||||
|
||||
# Create the actual resources.pack
|
||||
pack: $(TARGET)
|
||||
@echo "Creating resources.pack..."
|
||||
./$(TARGET) ../../data ../../resources.pack
|
||||
|
||||
# List contents of a pack
|
||||
list: $(TARGET)
|
||||
@echo "Listing pack contents..."
|
||||
./$(TARGET) --list ../../resources.pack
|
||||
|
||||
# Clean
|
||||
clean:
|
||||
@echo "Cleaning..."
|
||||
rm -f $(TARGET) test_resources.pack
|
||||
@echo "Clean complete"
|
||||
|
||||
.PHONY: all test pack list clean
|
||||
138
tools/pack_resources/pack_resources.cpp
Normal file
138
tools/pack_resources/pack_resources.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
// pack_resources.cpp
|
||||
// Tool to pack game resources into a single encrypted file
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Include the ResourcePack class
|
||||
#include "../../source/core/resources/resource_pack.hpp"
|
||||
|
||||
void printUsage(const char* program_name) {
|
||||
std::cout << "Usage:\n";
|
||||
std::cout << " " << program_name << " [input_dir] [output_file]\n";
|
||||
std::cout << " " << program_name << " --list <pack_file>\n";
|
||||
std::cout << "\n";
|
||||
std::cout << "Examples:\n";
|
||||
std::cout << " " << program_name << " data resources.pack\n";
|
||||
std::cout << " " << program_name << " --list resources.pack\n";
|
||||
std::cout << "\n";
|
||||
std::cout << "Options:\n";
|
||||
std::cout << " --list List contents of a pack file\n";
|
||||
}
|
||||
|
||||
auto handleDefaultPack() -> int {
|
||||
std::string input_dir = "data";
|
||||
std::string output_file = "resources.pack";
|
||||
|
||||
std::cout << "Using defaults:\n";
|
||||
std::cout << " Input: " << input_dir << "/\n";
|
||||
std::cout << " Output: " << output_file << "\n\n";
|
||||
|
||||
Resource::Pack pack;
|
||||
if (!pack.addDirectory(input_dir)) {
|
||||
std::cerr << "Error: Failed to add directory: " << input_dir << '\n';
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Add config/assets.yaml to pack (required for release builds)
|
||||
std::cout << "\nAdding config files...\n";
|
||||
if (!pack.addFile("config/assets.yaml", "config/assets.yaml")) {
|
||||
std::cerr << "Warning: Failed to add config/assets.yaml (optional)\n";
|
||||
}
|
||||
|
||||
if (!pack.savePack(output_file)) {
|
||||
std::cerr << "Error: Failed to save pack file: " << output_file << '\n';
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "\nSuccess! Pack created: " << output_file << '\n';
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto handleListContents(const std::string& pack_file) -> int {
|
||||
std::cout << "Loading pack: " << pack_file << "\n\n";
|
||||
|
||||
Resource::Pack pack;
|
||||
if (!pack.loadPack(pack_file)) {
|
||||
std::cerr << "Error: Failed to load pack file: " << pack_file << '\n';
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "\nPack Contents:\n";
|
||||
std::cout << "==============\n";
|
||||
|
||||
auto resources = pack.getResourceList();
|
||||
size_t total_size = 0;
|
||||
|
||||
for (const auto& resource : resources) {
|
||||
auto data = pack.getResource(resource);
|
||||
total_size += data.size();
|
||||
std::cout << " " << resource << " (" << data.size() << " bytes)\n";
|
||||
}
|
||||
|
||||
std::cout << "\nTotal Resources: " << resources.size() << '\n';
|
||||
std::cout << "Total Size: " << total_size << " bytes\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto handlePackDirectory(const std::string& input_dir, const std::string& output_file) -> int {
|
||||
std::cout << "Input: " << input_dir << "/\n";
|
||||
std::cout << "Output: " << output_file << "\n\n";
|
||||
|
||||
Resource::Pack pack;
|
||||
if (!pack.addDirectory(input_dir)) {
|
||||
std::cerr << "Error: Failed to add directory: " << input_dir << '\n';
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Add config/assets.yaml to pack (required for release builds)
|
||||
std::cout << "\nAdding config files...\n";
|
||||
if (!pack.addFile("config/assets.yaml", "config/assets.yaml")) {
|
||||
std::cerr << "Warning: Failed to add config/assets.yaml (optional)\n";
|
||||
}
|
||||
|
||||
if (!pack.savePack(output_file)) {
|
||||
std::cerr << "Error: Failed to save pack file: " << output_file << '\n';
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "\nSuccess! Pack created: " << output_file << '\n';
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto main(int argc, char* argv[]) -> int {
|
||||
std::cout << "Projecte 2026 - Resource Packer\n";
|
||||
std::cout << "======================================\n\n";
|
||||
|
||||
// Default behavior: pack data/ into resources.pack
|
||||
if (argc == 1) {
|
||||
return handleDefaultPack();
|
||||
}
|
||||
|
||||
// Help command
|
||||
if (argc == 2) {
|
||||
std::string arg = argv[1];
|
||||
if (arg == "--help" || arg == "-h") {
|
||||
printUsage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// List contents or pack directory
|
||||
if (argc == 3) {
|
||||
std::string arg1 = argv[1];
|
||||
std::string arg2 = argv[2];
|
||||
|
||||
if (arg1 == "--list" || arg1 == "-l") {
|
||||
return handleListContents(arg2);
|
||||
}
|
||||
|
||||
return handlePackDirectory(arg1, arg2);
|
||||
}
|
||||
|
||||
// Invalid arguments
|
||||
std::cerr << "Error: Invalid arguments\n\n";
|
||||
printUsage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
101
tools/shaders/compile_spirv.cmake
Normal file
101
tools/shaders/compile_spirv.cmake
Normal file
@@ -0,0 +1,101 @@
|
||||
# compile_spirv.cmake
|
||||
# Compila shaders GLSL a SPIR-V y genera headers C++ embebibles.
|
||||
# Multiplataforma: Windows, macOS, Linux (no requiere bash, xxd ni /tmp/).
|
||||
#
|
||||
# Invocado por CMakeLists.txt con:
|
||||
# cmake -D GLSLC=<path> -D SHADERS_DIR=<path> -D HEADERS_DIR=<path> -P compile_spirv.cmake
|
||||
#
|
||||
# También puede ejecutarse manualmente desde la raíz del proyecto:
|
||||
# cmake -D GLSLC=glslc -D SHADERS_DIR=data/shaders -D HEADERS_DIR=source/core/rendering/sdl3gpu -P tools/shaders/compile_spirv.cmake
|
||||
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
cmake_policy(SET CMP0007 NEW)
|
||||
|
||||
# Lista de shaders: fuente relativa a SHADERS_DIR
|
||||
set(SHADER_SOURCES
|
||||
"postfx.vert"
|
||||
"postfx.frag"
|
||||
"upscale.frag"
|
||||
"downscale.frag"
|
||||
"crtpi_frag.glsl"
|
||||
)
|
||||
|
||||
# Nombre de la variable C++ para cada shader (mismo orden)
|
||||
set(SHADER_VARS
|
||||
"kpostfx_vert_spv"
|
||||
"kpostfx_frag_spv"
|
||||
"kupscale_frag_spv"
|
||||
"kdownscale_frag_spv"
|
||||
"kcrtpi_frag_spv"
|
||||
)
|
||||
|
||||
# Flags extra de glslc para cada shader (vacío si no hay)
|
||||
set(SHADER_FLAGS
|
||||
""
|
||||
""
|
||||
""
|
||||
""
|
||||
"-fshader-stage=frag"
|
||||
)
|
||||
|
||||
list(LENGTH SHADER_SOURCES NUM_SHADERS)
|
||||
math(EXPR LAST_IDX "${NUM_SHADERS} - 1")
|
||||
|
||||
foreach(IDX RANGE ${LAST_IDX})
|
||||
list(GET SHADER_SOURCES ${IDX} SRC_NAME)
|
||||
list(GET SHADER_VARS ${IDX} VAR)
|
||||
list(GET SHADER_FLAGS ${IDX} EXTRA_FLAG)
|
||||
|
||||
# Derivar nombre del header desde la variable: kpostfx_vert_spv → postfx_vert_spv.h
|
||||
string(REGEX REPLACE "^k" "" HDR_BASE "${VAR}")
|
||||
set(SRC "${SHADERS_DIR}/${SRC_NAME}")
|
||||
set(SPV "${HEADERS_DIR}/${HDR_BASE}.spv")
|
||||
set(HDR "${HEADERS_DIR}/${HDR_BASE}.h")
|
||||
|
||||
message(STATUS "Compilando ${SRC} ...")
|
||||
|
||||
if(EXTRA_FLAG)
|
||||
execute_process(
|
||||
COMMAND "${GLSLC}" "${EXTRA_FLAG}" "${SRC}" -o "${SPV}"
|
||||
RESULT_VARIABLE GLSLC_RESULT
|
||||
ERROR_VARIABLE GLSLC_ERROR
|
||||
)
|
||||
else()
|
||||
execute_process(
|
||||
COMMAND "${GLSLC}" "${SRC}" -o "${SPV}"
|
||||
RESULT_VARIABLE GLSLC_RESULT
|
||||
ERROR_VARIABLE GLSLC_ERROR
|
||||
)
|
||||
endif()
|
||||
|
||||
if(NOT GLSLC_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "glslc falló para ${SRC}:\n${GLSLC_ERROR}")
|
||||
endif()
|
||||
|
||||
# Leer binario SPV como hex (sin separadores: "0302230700...")
|
||||
file(READ "${SPV}" HEX_DATA HEX)
|
||||
# Dividir en pares de caracteres hex → lista de bytes
|
||||
string(REGEX MATCHALL ".." BYTES "${HEX_DATA}")
|
||||
list(LENGTH BYTES NUM_BYTES)
|
||||
|
||||
# Construir el cuerpo del array C++ con un byte por línea
|
||||
set(ARRAY_BODY "")
|
||||
foreach(BYTE ${BYTES})
|
||||
string(APPEND ARRAY_BODY " 0x${BYTE},\n")
|
||||
endforeach()
|
||||
|
||||
file(WRITE "${HDR}"
|
||||
"#pragma once\n"
|
||||
"#include <cstddef>\n"
|
||||
"#include <cstdint>\n"
|
||||
"static const uint8_t ${VAR}[] = {\n"
|
||||
"${ARRAY_BODY}"
|
||||
"};\n"
|
||||
"static const size_t ${VAR}_size = ${NUM_BYTES};\n"
|
||||
)
|
||||
|
||||
file(REMOVE "${SPV}")
|
||||
message(STATUS " -> ${HDR} (${NUM_BYTES} bytes)")
|
||||
endforeach()
|
||||
|
||||
message(STATUS "Shaders SPIR-V compilados correctamente.")
|
||||
58
tools/shaders/compile_spirv.sh
Executable file
58
tools/shaders/compile_spirv.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compile Vulkan GLSL shaders to SPIR-V and update the C++ headers used by SDL3GPUShader.
|
||||
# Required: glslc (from Vulkan SDK or: brew install glslang / apt install glslang-tools)
|
||||
#
|
||||
# Run from the project root: tools/shaders/compile_spirv.sh
|
||||
|
||||
set -e
|
||||
|
||||
SHADERS_DIR="data/shaders"
|
||||
HEADERS_DIR="source/core/rendering/sdl3gpu"
|
||||
|
||||
if ! command -v glslc &> /dev/null; then
|
||||
echo "ERROR: glslc not found. Install Vulkan SDK or run:"
|
||||
echo " macOS: brew install glslang"
|
||||
echo " Linux: sudo apt install glslang-tools"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Compiling SPIR-V shaders..."
|
||||
|
||||
glslc "${SHADERS_DIR}/postfx.vert" -o /tmp/postfx.vert.spv
|
||||
glslc "${SHADERS_DIR}/postfx.frag" -o /tmp/postfx.frag.spv
|
||||
glslc "${SHADERS_DIR}/upscale.frag" -o /tmp/upscale.frag.spv
|
||||
glslc "${SHADERS_DIR}/downscale.frag" -o /tmp/downscale.frag.spv
|
||||
|
||||
echo "Generating C++ headers..."
|
||||
|
||||
xxd -i /tmp/postfx.vert.spv | \
|
||||
sed 's/unsigned char .*postfx_vert_spv\[\]/static const uint8_t kpostfx_vert_spv[]/' | \
|
||||
sed 's/unsigned int .*postfx_vert_spv_len/static const size_t kpostfx_vert_spv_size/' \
|
||||
> "${HEADERS_DIR}/postfx_vert_spv.h"
|
||||
|
||||
xxd -i /tmp/postfx.frag.spv | \
|
||||
sed 's/unsigned char .*postfx_frag_spv\[\]/static const uint8_t kpostfx_frag_spv[]/' | \
|
||||
sed 's/unsigned int .*postfx_frag_spv_len/static const size_t kpostfx_frag_spv_size/' \
|
||||
> "${HEADERS_DIR}/postfx_frag_spv.h"
|
||||
|
||||
xxd -i /tmp/upscale.frag.spv | \
|
||||
sed 's/unsigned char .*upscale_frag_spv\[\]/static const uint8_t kupscale_frag_spv[]/' | \
|
||||
sed 's/unsigned int .*upscale_frag_spv_len/static const size_t kupscale_frag_spv_size/' \
|
||||
> "${HEADERS_DIR}/upscale_frag_spv.h"
|
||||
|
||||
xxd -i /tmp/downscale.frag.spv | \
|
||||
sed 's/unsigned char .*downscale_frag_spv\[\]/static const uint8_t kdownscale_frag_spv[]/' | \
|
||||
sed 's/unsigned int .*downscale_frag_spv_len/static const size_t kdownscale_frag_spv_size/' \
|
||||
> "${HEADERS_DIR}/downscale_frag_spv.h"
|
||||
|
||||
# Prepend required includes to the headers
|
||||
for f in "${HEADERS_DIR}/postfx_vert_spv.h" "${HEADERS_DIR}/postfx_frag_spv.h" "${HEADERS_DIR}/upscale_frag_spv.h" "${HEADERS_DIR}/downscale_frag_spv.h"; do
|
||||
echo -e "#pragma once\n#include <cstdint>\n#include <cstddef>\n$(cat "$f")" > "$f"
|
||||
done
|
||||
|
||||
echo "Done. Headers updated in ${HEADERS_DIR}/"
|
||||
echo " postfx_vert_spv.h"
|
||||
echo " postfx_frag_spv.h"
|
||||
echo " upscale_frag_spv.h"
|
||||
echo " downscale_frag_spv.h"
|
||||
echo "Rebuild the project to use the new shaders."
|
||||
Reference in New Issue
Block a user