mount C ".\Kings of the beach"
Buenas noches,
Os dejo la imagen de diskette que he encontrado, la ruleta de códigos interactiva y la medicina.
Aceptará cualquier cadena introducida.
En este caso, son 3 los archivos medicados, ya que según los gráficos seleccionados, carga un archivo u otro.
Probado en DOSBox 0.74-3 sin finalizar.
:disco: 1x Diskette 3.5" DD .IMG (https://drive.google.com/file/d/1RY5lC_p22TneH3IAM2Mq_cHxnFsXL5uU/view?usp=sharing) :en: (210 KB)
:zip: Medicina (https://drive.google.com/file/d/1IfDsGTrmhfHDSvfmSJWEpGIRbA_8EUNI/view?usp=sharing) :en: (61 KB)
:zip: Ruleta Códigos .HTM (https://drive.google.com/file/d/18m9CnfDjWob6k2vSb2QgM90eOvS0gafh/view?usp=sharing) (2,62 MB)
Salu2
| #!/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() |