Autor Tema: Kings of the Beach  (Leído 14257 veces)

0 Usuarios y 1 Visitante están viendo este tema.

Desconectado Darkoz Krull

  • Amiga A1000
  • **
  • Mensajes: 459
  • País: ar
  • Sexo: Masculino
  • Fecha de registro: Diciembre 14, 2017, 15:03:14 pm
    • Ver Perfil
    • Email
Re: Kings of the Beach
« Respuesta #15 en: Enero 27, 2025, 13:35:11 pm »
Gracias, otro para la colección.
¿No tendrá un mod de Dead or Alive Extreme?  :lol:



Desconectado marianolloret

  • Calculadora
  • Mensajes: 25
  • País: es
  • Fecha de registro: Julio 27, 2020, 15:28:54 pm
    • Ver Perfil
    • Email
Re: Kings of the Beach
« Respuesta #16 en: Julio 22, 2026, 16:26:21 pm »
Como cada verano, he vuelto a engancharme a este juego. Esta vez, aprovechando el auge de la inteligencia artificial, me pregunté si podría usarla para modificarlo. Después de unas cuantas pruebas, puedo decir que algo he conseguido.

Le pregunté a Gemini si existía algún sitio web en el que se explicara técnicamente el funcionamiento interno del juego, y me recomendó la siguiente página:

https://moddingwiki.shikadi.net/wiki/Kings_of_the_Beach_PAK_Format

En ella se documenta con detalle la estructura interna de los archivos .PAK, explicando el sistema de compresión RLE utilizado por el juego, el formato de las cabeceras y la organización de los datos una vez descomprimidos. Además, describe cómo se representan las imágenes en formato EGA de 4 bits por planos, el proceso necesario para reconstruirlas correctamente y las particularidades de algunos archivos parciales utilizados para superponer gráficos sobre otros. La documentación también incluye ejemplos de la estructura binaria y herramientas compatibles para visualizar, convertir y modificar estos archivos, convirtiéndose en una referencia imprescindible para cualquiera que quiera analizar o modificar los recursos gráficos de Kings of the Beach.

Pues bien, con esto le pedí a Claude si podía crear un script en Python para convertir los ficheros de PAK a PNG y para mi sorpresa lo hizo en menos de un minuto:

#!/usr/bin/env python3
"""
kotb_pak_converter.py
======================

Convierte entre el formato de imagen .PAK del juego "Kings of the Beach"
(EGA, 4 bits, planar, comprimido con RLE de tipo "code-based") y PNG.

Uso:
    # PAK -> PNG
    python3 kotb_pak_converter.py pak2png ENTRADA.PAK SALIDA.png

    # PNG -> PAK
    python3 kotb_pak_converter.py png2pak ENTRADA.png SALIDA.PAK [--height N] [--no-partial]

    # Autodetectar dirección según la extensión de los ficheros
    python3 kotb_pak_converter.py convert ENTRADA SALIDA

Notas:
- La paleta usada es la paleta EGA estándar de 16 colores.
- Al convertir PNG -> PAK, los colores de la imagen se ajustan (nearest
  color) a la paleta EGA de 16 colores más cercana.
- El ancho de la imagen debe ser múltiplo de 8 (requisito del formato).
- Si la imagen PNG tiene un bloque de líneas totalmente negras (índice de
  color 0) al final, se codifica como "archivo parcial" (igual que hacen
  los ficheros de canchas del juego), salvo que se use --no-partial.
"""

import argparse
import struct
import sys
from pathlib import Path

try:
    from PIL import Image
except ImportError:
    sys.exit("Este script necesita Pillow. Instálalo con: pip install Pillow")


# ---------------------------------------------------------------------------
# Paleta EGA estándar (16 colores)
# ---------------------------------------------------------------------------
EGA_PALETTE = [
    (0, 0, 0), (0, 0, 170), (0, 170, 0), (0, 170, 170),
    (170, 0, 0), (170, 0, 170), (170, 85, 0), (170, 170, 170),
    (85, 85, 85), (85, 85, 255), (85, 255, 85), (85, 255, 255),
    (255, 85, 85), (255, 85, 255), (255, 255, 85), (255, 255, 255),
]


# ---------------------------------------------------------------------------
# RLE "code-based" (bit alto = copiar literal / bit alto apagado = repetir)
# ---------------------------------------------------------------------------
def rle_decompress(comp: bytes) -> bytes:
    out = bytearray()
    i = 0
    n = len(comp)
    while i < n:
        code = comp
        i += 1
        if code & 0x80:
            amt = code & 0x7F
            out.extend(comp[i:i + amt])
            i += amt
        else:
            amt = code
            if amt == 0:
                # Código sin uso práctico (repetir 0 veces); lo saltamos.
                continue
            val = comp
            i += 1
            out.extend([val] * amt)
    return bytes(out)


def rle_compress(data: bytes) -> bytes:
    out = bytearray()
    literal_buf = bytearray()

    def flush_literal():
        pos = 0
        while pos < len(literal_buf):
            chunk = literal_buf[pos:pos + 127]
            out.append(0x80 | len(chunk))
            out.extend(chunk)
            pos += len(chunk)
        literal_buf.clear()

    i = 0
    n = len(data)
    while i < n:
        run = 1
        while i + run < n and data[i + run] == data and run < 127:
            run += 1
        if run >= 2:
            flush_literal()
            out.append(run)          # bit alto apagado => Repeat
            out.append(data)
            i += run
        else:
            literal_buf.append(data)
            i += 1
    flush_literal()
    return bytes(out)


# ---------------------------------------------------------------------------
# PAK -> PNG
# ---------------------------------------------------------------------------
def pak_to_png(pak_path: str, png_path: str) -> None:
    raw = Path(pak_path).read_bytes()
    if len(raw) < 4:
        raise ValueError("Fichero PAK demasiado pequeño / inválido")

    declen = raw[-2] | (raw[-1] << 8)
    comp = raw[:-2]

    decompressed = rle_decompress(comp)
    if len(decompressed) != declen:
        print(f"Aviso: longitud descomprimida ({len(decompressed)}) "
              f"no coincide con la esperada ({declen})", file=sys.stderr)

    bytewidth = decompressed[0]
    height = decompressed[1]
    imgdata = decompressed[2:]

    width = bytewidth * 8
    lines_available = len(imgdata) // (bytewidth * 4)
    if lines_available < height:
        print(f"Info: fichero parcial -> {lines_available} de {height} líneas "
              f"presentes (el resto se rellena en negro)", file=sys.stderr)

    img = Image.new("RGB", (width, height), EGA_PALETTE[0])
    pixels = img.load()

    for line in range(lines_available):
        base = line * bytewidth * 4
        planes = [
            imgdata[base + p * bytewidth: base + (p + 1) * bytewidth]
            for p in range(4)
        ]
        for xb in range(bytewidth):
            b0, b1, b2, b3 = (planes[0][xb], planes[1][xb],
                              planes[2][xb], planes[3][xb])
            for bit in range(8):
                shift = 7 - bit
                color_index = (
                    ((b0 >> shift) & 1)
                    | (((b1 >> shift) & 1) << 1)
                    | (((b2 >> shift) & 1) << 2)
                    | (((b3 >> shift) & 1) << 3)
                )
                x = xb * 8 + bit
                pixels[x, line] = EGA_PALETTE[color_index]

    img.save(png_path)
    print(f"OK: {pak_path} -> {png_path} ({width}x{height}, "
          f"{lines_available} líneas de datos reales)")


# ---------------------------------------------------------------------------
# PNG -> PAK
# ---------------------------------------------------------------------------
def _nearest_ega_index(rgb):
    r, g, b = rgb[:3]
    best_i, best_d = 0, None
    for i, (pr, pg, pb) in enumerate(EGA_PALETTE):
        d = (r - pr) ** 2 + (g - pg) ** 2 + (b - pb) ** 2
        if best_d is None or d < best_d:
            best_d, best_i = d, i
    return best_i


def png_to_pak(png_path: str, pak_path: str, height: int = None,
                allow_partial: bool = True) -> None:
    img = Image.open(png_path).convert("RGB")
    width, img_height = img.size

    if width % 8 != 0:
        raise ValueError(f"El ancho ({width}) debe ser múltiplo de 8")
    if width > 320 or img_height > 200:
        raise ValueError("El tamaño máximo soportado es 320x200")

    bytewidth = width // 8
    full_height = height if height is not None else img_height
    if img_height > full_height:
        raise ValueError("La imagen es más alta que la 'Height' indicada")

    pixels = img.load()

    # Índice de color EGA (0-15) por cada píxel
    indices = [
        [_nearest_ega_index(pixels[x, y]) for x in range(width)]
        for y in range(img_height)
    ]
    # Rellenamos hasta full_height con líneas negras (índice 0) si hiciera falta
    for _ in range(full_height - img_height):
        indices.append([0] * width)

    # Detectar líneas negras finales para codificar como fichero "parcial"
    lines_to_store = full_height
    if allow_partial:
        while lines_to_store > 0 and all(v == 0 for v in indices[lines_to_store - 1]):
            lines_to_store -= 1
        if lines_to_store == 0:
            lines_to_store = full_height  # imagen totalmente negra: guardar completa

    imgdata = bytearray()
    for line in range(lines_to_store):
        row = indices[line]
        planes = [bytearray(bytewidth) for _ in range(4)]
        for xb in range(bytewidth):
            for bit in range(8):
                x = xb * 8 + bit
                val = row

                shift = 7 - bit
                for p in range(4):
                    if (val >> p) & 1:
                        planes[p][xb] |= (1 << shift)
        for p in range(4):
            imgdata.extend(planes[p])

    decompressed = bytes([bytewidth, full_height]) + bytes(imgdata)
    declen = len(decompressed)
    if declen > 0xFFFF:
        raise ValueError("Los datos descomprimidos exceden 65535 bytes")

    compressed = rle_compress(decompressed)

    out = compressed + struct.pack("<H", declen)
    Path(pak_path).write_bytes(out)

    partial_note = (f" (parcial: {lines_to_store}/{full_height} líneas)"
                     if lines_to_store < full_height else "")
    print(f"OK: {png_path} -> {pak_path} ({width}x{full_height}){partial_note}")


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main():
    parser = argparse.ArgumentParser(description=__doc__,
                                      formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = parser.add_subparsers(dest="mode", required=True)

    p1 = sub.add_parser("pak2png", help="Convierte un fichero .PAK a .png")
    p1.add_argument("input")
    p1.add_argument("output")

    p2 = sub.add_parser("png2pak", help="Convierte un fichero .png a .PAK")
    p2.add_argument("input")
    p2.add_argument("output")
    p2.add_argument("--height", type=int, default=None,
                     help="Alto declarado en la cabecera (por defecto, el alto real del PNG)")
    p2.add_argument("--no-partial", action="store_true",
                     help="Fuerza la codificación completa, sin recortar líneas negras finales")

    p3 = sub.add_parser("convert", help="Autodetecta la dirección según la extensión")
    p3.add_argument("input")
    p3.add_argument("output")
    p3.add_argument("--height", type=int, default=None)
    p3.add_argument("--no-partial", action="store_true")

    args = parser.parse_args()

    if args.mode == "pak2png":
        pak_to_png(args.input, args.output)
    elif args.mode == "png2pak":
        png_to_pak(args.input, args.output, height=args.height,
                   allow_partial=not args.no_partial)
    elif args.mode == "convert":
        in_ext = Path(args.input).suffix.lower()
        out_ext = Path(args.output).suffix.lower()
        if in_ext == ".pak" and out_ext == ".png":
            pak_to_png(args.input, args.output)
        elif in_ext == ".png" and out_ext == ".pak":
            png_to_pak(args.input, args.output, height=args.height,
                       allow_partial=not args.no_partial)
        else:
            sys.exit("No se pudo determinar la dirección de conversión "
                      "a partir de las extensiones de fichero.")


if __name__ == "__main__":
    main()

ahora se ha abierto la posibilidad de modificar los gráficos del juego.

PD: Para mi sorpresa, Claude me sugirió que si le indicaba los canvios que quería hacer en el fichero PAK intentaría realizarlos él sin la necesidad de convertir el fichero a PNG y luego de nuevo a PAK  :huh:
« Última modificación: Julio 22, 2026, 16:30:32 pm por marianolloret »

Desconectado Neville

  • Shodan
  • *****
  • Mensajes: 7287
  • País: es
  • Fecha de registro: Diciembre 17, 2010, 22:28:17 pm
    • Ver Perfil
Re: Kings of the Beach
« Respuesta #17 en: Julio 22, 2026, 16:44:52 pm »
La verdad es que lo de la AI abre toda una puerta al tema de los hacks y las modificaciones de los juegos antiguos. Alguna vez me he preguntado si sería posible un port para Windows o solucionar los bugs de tal o cuál juego.

Puede que no sea la panacea, pero la posibilidad está ahí.



Desconectado marianolloret

  • Calculadora
  • Mensajes: 25
  • País: es
  • Fecha de registro: Julio 27, 2020, 15:28:54 pm
    • Ver Perfil
    • Email
Re: Kings of the Beach
« Respuesta #18 en: Julio 22, 2026, 16:53:43 pm »
Estoy pensando en si sería posible desensamblar el juego mediante una herramienta de IA ... que generara un fichero en lenguaje C con el código contextualizado y bien documentado.