Initial commit: PCC-Decoder (mit Xmax/Ymax-Bugfix), Notes, DOSBox-Config

- tools/pcc_to_png.py: PCX-artiger Decoder fuer die 77 PCC-Vollbild-/Logo-
  Dateien, mit Fix fuer falsch gelesene Width/Height (muss aus Xmax-Xmin+1/
  Ymax-Ymin+1 statt Offset 12-15 berechnet werden)
- tools/split_pre.py: Extrahiert alle 181 Dateien aus PCKELL.PRE
- png_out/: alle 77 konvertierten PNGs (Arbeitsergebnis, kein Original-Asset)
- NOTES.md: vollstaendiger Analyse-Stand inkl. offener Punkte
- dosbox/dosbox.conf: funktionierende Testkonfiguration (IRQ-Fix)

Bewusst ausgeschlossen (.gitignore): raw/ und extracted_pre/, da Original-
Spieldaten von Kellogg's Tony and Friends (1994), keine Weiterverbreitung.
This commit is contained in:
ARIA
2026-07-22 07:20:13 +00:00
commit 3c650e6aef
82 changed files with 690 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Finaler PCC-Decoder fuer Kellogg's Tony and Friends (2026-07-22, ARIA).
Format (verifiziert gegen MENU.PCC/RAUSER1-3/FACTOR5, visuell + byte-exakt):
- Byte 0-15: echter PCX-Header-Anfang (Manufacturer=0x0A, Version=5,
Encoding=1/RLE, BPP=8, dann Xmin/Ymin/Xmax/Ymax als LE16 bei Offset 4-11).
WICHTIG (Bugfix 2026-07-22): Width/Height MUESSEN aus Xmax-Xmin+1 /
Ymax-Ymin+1 berechnet werden. Die 2 LE16-Werte bei Offset 12-15 sehen fuer
Vollbild-Screens (320x200) zufaellig identisch aus und wurden erst dafuer
gehalten -- sind aber tatsaechlich NICHT die Bilddimensionen (vermutlich
ein DPI/Reserved-Feld wie im echten 128-Byte-PCX-Header), sondern ein
Konstantwert der bei kleinen Sprites/Logos (z.B. RAUSER1.PCC: 182x46,
FAC0.PCC: 16x16) komplett falsch war und zu kaputten/leeren Bildern fuehrte.
Mit dem Xmax/Ymax-Fix konsumiert z.B. FACTOR5.PCC exakt 100% der
komprimierten Bytes (51609/51609) statt vorher mit falscher Hoehe (200
statt echten 199) zu ueberlaufen.
- Byte 16 .. (len-769): RLE-komprimierte Pixel-Indexdaten, SCANLINE-weise
decodiert (pro Zeile wird bis width Pixel gefuellt, ueberschuessige Pixel
eines Runs am Zeilenende werden verworfen -- klassisches PCX-Verhalten).
RLE-Tupel: Byte mit oberen 2 Bits gesetzt (0xC0-0xFF) = Lauflaenge (&0x3F),
gefolgt von einem Wert-Byte. Sonst literaler Pixel.
- Letzte 769 Bytes: 0x0C-Marker + 768 Byte (256 x RGB) eingebettete Palette
(klassische PCX-v5-256-Farben-Erweiterung). PRO DATEI eigene Palette,
keine globale Palette noetig.
BEKANNTER OFFENER BUG (Stand 2026-07-22, noch nicht geloest):
KELLOGGS.PCC und KARTE.PCC (volle 320x200-Screens) zeigen weiterhin einen
lokalen Deko-Fehler (ein "Kerbe"/Notch-Artefakt rechts neben dem Kellogg's-
Schriftzug, roter/gelber Fleck der nicht ins Referenz-Screenshot passt).
Ausgeschlossen als Ursache: (a) horizontales Rollen/Verschieben des ganzen
Bilds -- getestet mit 18 Shift-Kandidaten, Artefakt bleibt IMMER an
derselben Position relativ zum Bildinhalt, nicht zur Leinwand -> kein
Rotations-/Scroll-Bug. (b) Zeilenweises Verwerfen von RLE-Overshoot --
zeilenbasierte und "kontinuierliche" (ohne Row-Reset) Decodierung liefern
BYTE-IDENTISCHE Ergebnisse fuer diese Dateien. Vermutung: entweder eine
Palette-Fehlzuordnung fuer einzelne Indizes, oder eine RLE-Ambiguitaet bei
literalen Pixelwerten >= 0xC0 (die durch (b&0xC0)==0xC0 faelschlich als
Lauflaengen-Token statt als literaler Indexwert interpretiert werden
koennten) -- noch nicht verifiziert. FACTOR5.PCC's "Geister"-Doppellogo
ist dagegen vermutlich KEIN Bug, sondern ein Reflexions-/Schatten-
Designelement (Byte-Konsum ist bei diesem File exakt 100%).
Nutzung: python3 pcc_to_png.py <input.PCC> <output.png>
Schreibt ein PPM und konvertiert via ImageMagick `convert` zu PNG.
"""
import sys, struct, subprocess, os
def decode_pcc(data):
manuf, version, encoding, bpp = data[0], data[1], data[2], data[3]
xmin, ymin, xmax, ymax = struct.unpack('<HHHH', data[4:12])
width, height = xmax - xmin + 1, ymax - ymin + 1
pal_start = len(data) - 769
marker = data[pal_start]
pixel_region = data[16:pal_start]
pal_bytes = data[pal_start+1:pal_start+1+768]
palette = [(pal_bytes[i], pal_bytes[i+1], pal_bytes[i+2]) for i in range(0, 768, 3)]
out = bytearray()
i = 0
n = len(pixel_region)
for row in range(height):
row_out = bytearray()
while len(row_out) < width and i < n:
b = pixel_region[i]; i += 1
if (b & 0xC0) == 0xC0:
count = b & 0x3F
if i >= n:
break
val = pixel_region[i]; i += 1
row_out.extend([val] * count)
else:
row_out.append(b)
row_out = row_out[:width]
if len(row_out) < width:
row_out.extend([0] * (width - len(row_out)))
out.extend(row_out)
return dict(width=width, height=height, marker_ok=(marker == 0x0C),
pixels=bytes(out), palette=palette,
consumed=i, pixel_region_len=n)
def write_png(pixels, palette, width, height, out_png):
ppm = out_png + '.ppm'
with open(ppm, 'wb') as f:
f.write(f'P6\n{width} {height}\n255\n'.encode())
buf = bytearray()
for px in pixels[:width*height]:
r, g, b = palette[px]
buf.extend([r, g, b])
f.write(bytes(buf))
subprocess.run(['convert', ppm, out_png], check=True)
os.remove(ppm)
def main():
inp, outp = sys.argv[1], sys.argv[2]
with open(inp, 'rb') as f:
data = f.read()
res = decode_pcc(data)
print(f"{inp}: {res['width']}x{res['height']} marker_ok={res['marker_ok']} "
f"consumed={res['consumed']}/{res['pixel_region_len']}")
write_png(res['pixels'], res['palette'], res['width'], res['height'], outp)
print(f"-> {outp}")
if __name__ == '__main__':
main()
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""PCKELL.PRE Splitter v7 -- FINAL (2026-07-22)
Format komplett verstanden:
- .BOB-Eintraege (32x): kein Tag-Slot, Header direkt per Pfad-Praefix-Suche
gefunden ("H:\\WIWA\\ENGINE\\GAMEDATA\\<name>"), Header = Fundstelle-4.
- Alle Nicht-BOB-Eintraege (PCC/MAP/ARE/ICO/SAM/TFX, 146x aus APPEND.LST,
PLUS 3 weitere System-Dateien ohne APPEND.LST-Eintrag): Tag-Slot
[1-Byte-Namenslaenge][Dateiname][Fuellmuell bis 256 Byte], danach
[4-Byte-LE-Laenge][Rohdaten].
WICHTIGER FUND: APPEND.LST listet nur die 178 Spiel-Assets. PCKELL.PRE
enthaelt zusaetzlich, im GLEICHEN Tag-Format, direkt im Anschluss an den
letzten APPEND.LST-Eintrag (TITEL2.SAM), noch 3 System-/Runtime-Dateien:
DPMI16BI.OVL, RTM.EXE, START.EXE (die DOS-Extender-Runtime -- macht Sinn,
das ist ein self-contained Redistributable). Der letzte dieser 3 Eintraege
endet EXAKT auf EOF (0x238966) -- damit ist das Format zu 100% verifiziert,
keine Bytes bleiben unerklaert.
Ergebnis: 178 (APPEND.LST) + 3 (System) = 181 Dateien total.
"""
import struct
import os
RAW = "/home/aria/kellogg_remake/raw/PCKELL.PRE"
APPEND_LST = "/home/aria/kellogg_remake/raw/APPEND.LST"
OUT_DIR = "/home/aria/kellogg_remake/extracted_pre"
with open(RAW, "rb") as f:
data = f.read()
with open(APPEND_LST) as f:
names = [l.strip() for l in f if l.strip()]
# die 3 unlisted System-Dateien, empirisch gefunden direkt im Anschluss
EXTRA_NAMES = ["DPMI16BI.OVL", "RTM.EXE", "START.EXE"]
PATH_PREFIX = b"H:\\WIWA\\ENGINE\\GAMEDATA\\"
N = len(data)
pos = 0x100
entries = []
all_names = names + EXTRA_NAMES
for i, name in enumerate(all_names):
is_bob = name.upper().endswith(".BOB")
if is_bob:
marker = PATH_PREFIX + name.encode("ascii")
p = data.find(marker, pos)
if p == -1:
print(f"FEHLER: Pfad fuer #{i} ({name}) nicht gefunden ab {pos:#x}")
break
header_start = p - 4
length = struct.unpack_from("<I", data, header_start)[0]
path_start = p
nul = data.index(b"\x00", path_start)
path = data[path_start:nul].decode("cp437", errors="replace")
data_start = nul + 1
else:
tb = name.encode("ascii")
marker = bytes([len(tb)]) + tb
tag_pos = data.find(marker, pos)
if tag_pos == -1:
tag_pos = data.find(marker, max(0, pos - 300))
if tag_pos == -1:
print(f"FEHLER: Tag fuer #{i} ({name}) nicht gefunden ab {pos:#x}")
break
header_start = tag_pos + 256
length = struct.unpack_from("<I", data, header_start)[0]
path = None
data_start = header_start + 4
data_end = data_start + length
if data_end > N:
print(f"FEHLER: Datenende {data_end:#x} > Dateigroesse bei #{i} ({name}) length={length}")
break
entries.append((name, header_start, length, data_start, data_end, path))
pos = data_end
print(f"{len(entries)} von {len(all_names)} Eintraegen gefunden.")
if entries:
print(f"Letztes Datenende: {entries[-1][4]:#x} von {N:#x} (Match: {entries[-1][4] == N})")
mism = sum(1 for name, hs, length, ds, de, path in entries
if path is not None and path.split("\\")[-1].upper() != name.upper())
print(f"BOB Pfad-Mismatches: {mism}")
if len(entries) == len(all_names) and entries[-1][4] == N and mism == 0:
os.makedirs(OUT_DIR, exist_ok=True)
for name, hs, length, ds, de, path in entries:
with open(os.path.join(OUT_DIR, name), "wb") as out:
out.write(data[ds:de])
print(f">>> ERFOLG: {len(entries)} Dateien geschrieben nach {OUT_DIR}")
import collections
ext_count = collections.Counter(n.split(".")[-1].upper() for n, *_ in entries)
print("Typen:", dict(ext_count))
else:
print(">>> Unvollstaendig.")
for e in entries[-5:]:
print(" ", e[0], hex(e[1]), e[2])