PCC-Decoder-Bugfix: RLE-Runs duerfen Zeilengrenzen ueberschreiten (Row-Reset war falsch)
Stefan hat zurecht bemaengelt dass KARTE.PCC/KELLOGGS.PCC verzerrt/verrutscht
aussehen (Smacks-Frosch an falscher Position, Notch-Fehlstelle im Logo-Rahmen).
Root Cause: Der Decoder hat RLE-Runs am Ende jeder Bildzeile hart abgeschnitten
und den Rest verworfen (klassisches PCX-Verhalten angenommen). Dieses Format
haelt sich aber NICHT an die PCX-Konvention 'ein Run ueberschreitet nie eine
Zeile' -- Runs laufen frei ueber Zeilengrenzen. Verifiziert per Pixel-fuer-Pixel-
Vergleich gegen echten DOSBox-Screenshot (world map): mit durchgehender
Dekodierung (kein Row-Reset) ist das Ergebnis jetzt pixel-identisch zur Referenz.
- tools/pcc_to_png.py: Row-Reset entfernt, decodiert jetzt width*height Pixel
am Stueck aus dem RLE-Strom
- png_out/: alle 77 PNGs mit dem Fix neu generiert
- tools/wip_bob_sprite_research/: Backup der laufenden (noch ungeloesten)
BOB-Sprite-Format-Exploration von aria-wohnung, damit nichts bei einem
VM-Neustart verloren geht
- NOTES.md: Root-Cause-Analyse dokumentiert, alte Fehldiagnose ('byte-identisch
zwischen Row-Reset und kontinuierlich') korrigiert, Rauser-Intro-Frage
beantwortet (3 Frames vorhanden, Text-Logo, keine Video-Datei)
@@ -465,3 +465,85 @@ Original-Asset-Material, nur unverpackt). Mit eingecheckt: `NOTES.md`,
|
||||
`tools/` (unser Code: `split_pre.py`, `pcc_to_png.py`), `dosbox/`
|
||||
(Test-Konfiguration), `png_out/` (unsere konvertierten PNG-Ergebnisse,
|
||||
408 KB, reine Arbeitsergebnisse kein Originalmaterial).
|
||||
|
||||
## KELLOGGS.PCC/KARTE.PCC-Notch-Artefakt — GELOEST (2026-07-22, ~09:30 Ortszeit)
|
||||
|
||||
**Die Notiz oben ("Ausgeschlossen als Ursache ... Zeilenweises Verwerfen von
|
||||
RLE-Overshoot ... liefern byte-identische Ergebnisse") war FALSCH.** Das war
|
||||
ein Messfehler einer frueheren Session: der "kontinuierliche" Testdecoder
|
||||
hat versehentlich weiterhin die width/height aus Offset 12-15 gelesen (dem
|
||||
VERWORFENEN Bugfix-1-Feld) statt aus Xmax/Ymax — dadurch kam bei den beiden
|
||||
320x200-Vollbildern zufaellig das gleiche (korrekte) Ergebnis raus wie beim
|
||||
zeilenbasierten Decoder, und der Test schien zu bestaetigen, dass beide
|
||||
Methoden aequivalent sind. Sind sie nicht.
|
||||
|
||||
**Neu verifiziert mit einem sauberen Vergleichstest** (`Buffer.compare` in
|
||||
Node.js, unabhaengige Re-Implementierung): zeilenbasiertes Decoding
|
||||
(Row-Reset, discard Overflow am Zeilenende) und durchgehendes Decoding
|
||||
(RLE-Runs duerfen ueber Zeilengrenzen hinweglaufen) liefern **NICHT**
|
||||
identische Ergebnisse — 216 von 64000 Pixeln unterscheiden sich bei
|
||||
KARTE.PCC, 293 von 64000 bei KELLOGGS.PCC. Klingt nach wenig (<0.5%), reicht
|
||||
aber, um ganze Bildbereiche sichtbar zu verschieben, weil ab dem ersten
|
||||
"verlorenen" Ueberlauf-Pixel alle nachfolgenden Zeilen einen Pixel weiter
|
||||
"nachruecken".
|
||||
|
||||
**Beweis per Ground-Truth-Vergleich:** DOSBox-Screenshot
|
||||
`aria_kellogg_worldmap.png` (1024x768 mit schwarzen Balken) exakt auf den
|
||||
reinen Spielbereich zugeschnitten (`convert -fuzz 5% -trim` → 640x400,
|
||||
= 2x 320x200) und auf 320x200 runterskaliert
|
||||
(`/shared/uploads/aria_karte_reference_320.png`). Nebeneinander-Vergleich
|
||||
mit dem durchgehend (ohne Row-Reset) decodierten `KARTE.PCC`
|
||||
(`/shared/uploads/aria_karte_compare2.png`): **pixel-identisch** — Berge,
|
||||
Schloss, Drache, Tiger, Affe/Coco, Frosch, Teich, Haus, Voegel-Ecke, alles
|
||||
exakt an der richtigen Stelle. Das Row-Reset-Verhalten war der einzige
|
||||
Bug; es gibt kein horizontales Rollen, keine Palette-Fehlzuordnung, keine
|
||||
RLE-Ambiguitaet bei Werten >=0xC0.
|
||||
|
||||
**Fix:** `tools/pcc_to_png.py` decodiert jetzt global `width*height` Pixel
|
||||
aus dem RLE-Strom OHNE pro Zeile abzuschneiden (kein Row-Reset mehr). Alle
|
||||
77 PNGs in `png_out/` wurden mit diesem finalen Decoder neu erzeugt
|
||||
(diesmal wirklich verifiziert, nicht nur "sieht plausibel aus"). Kleine
|
||||
Sprites/Logos waren von diesem Bug kaum betroffen (selten ein Run, der eine
|
||||
Zeilengrenze ueberschreitet) — deshalb fiel es dort nie auf, nur bei den
|
||||
zwei grossen 320x200-Vollbildern.
|
||||
|
||||
**Rauser-Intro bestaetigt (Frage von Stefan):** `RAUSER1/2/3.PCC` sind schon
|
||||
laenger extrahiert und liegen jetzt mit dem finalen Decoder sauber decodiert
|
||||
in `png_out/`. Es ist ein Text-Logo "RAUSER" + "ADVERTAINMENT" (Studio-Card),
|
||||
als 3 Bild-Frames — vermutlich fuer einen Scroll-/Fade-Effekt, den die
|
||||
Original-Engine (RTM.EXE) zur Laufzeit zusammensetzt, KEIN Video/keine
|
||||
Animation-Datei. Fuers Remake heisst das: die 3 Frames sind da, die
|
||||
Overlay-/Scroll-Logik muss im Python-Port nachgebaut werden (vermutlich
|
||||
simpel: Frame croppen + ueber Zeit einblenden, sobald wir wissen wie die
|
||||
Original-Sequenz zeitlich ablaeuft — dafuer ggf. DOSBox-Referenzvideo/
|
||||
Screenshots in der Sequenz aufnehmen, spaeterer Schritt).
|
||||
|
||||
## BOB-Sprite-Format — Exploration laeuft, noch nicht geloest (Stand 09:30)
|
||||
|
||||
Auf `aria-wohnung` (`/home/aria/kellogg_remake/tools/`) lief parallel schon
|
||||
einiges an Exploration fuer das naechste Format (BOB-Sprites): Brute-Force-
|
||||
Breitensuche (`bob_width_search.py`), Header-Diff zwischen aehnlich grossen
|
||||
Dateien (`bob_header_diff.py`, `bob_header_search.py`), Histogramm-Analyse
|
||||
(`bob_histogram.py`), Sichtgitter-Rendering mit mehreren Breiten-Kandidaten
|
||||
(`bob_render_grid.py`/`bob_render_grid2.py`), sowie erste Versuche die
|
||||
`PCKELL.DAT`-Offset-Tabelle zu parsen (`dat_analyze.py`/`dat_analyze2.py`,
|
||||
`dat_table2.py`, `dat_u16.py`). **Kein Ergebnis/Durchbruch bisher** — alles
|
||||
noch offene Exploration, keiner der Scripts hat schon eine verifizierte
|
||||
Loesung. Zur Sicherheit (Stefans Wunsch: nichts soll bei einem VM-Neustart
|
||||
verloren gehen) sind alle diese Scripts jetzt zusaetzlich hier im
|
||||
Projekt gesichert unter `tools/wip_bob_sprite_research/` (noch nicht
|
||||
aufgeraeumt/nicht final, bewusst als "work in progress" separiert von den
|
||||
fertigen Tools `split_pre.py`/`pcc_to_png.py`).
|
||||
|
||||
## Naechste Schritte (Prioritaet, Stand 09:30)
|
||||
1. **BOB/SRB-Sprite-Format weiter reversen** — siehe `tools/wip_bob_sprite_research/`
|
||||
fuer den aktuellen (unfertigen) Exploration-Stand. Naechster konkreter
|
||||
Schritt: `PCKELL.DAT`-Offset-Tabelle sauber mit den 32 `SRB.LST`-Namen
|
||||
abgleichen, danach `A.BOB` (kleinstes Beispiel) testdecodieren.
|
||||
2. Rauser-Intro-Sequenz (3 Frames) zu einer zusammengesetzten Vorschau
|
||||
rendern/dokumentieren, sobald Zeit ist — niedrige Prioritaet, Assets sind
|
||||
schon da.
|
||||
3. MAP/ARE-Levelformat reversen.
|
||||
4. Python/pygame-Projektstruktur aufsetzen (PCC-Loader ist fertig, muss nur
|
||||
noch von PPM/ImageMagick-Zwischenschritt auf direktes Pillow-basiertes
|
||||
PNG/Surface-Schreiben umgestellt werden fuer den finalen Port).
|
||||
|
||||
|
Before Width: | Height: | Size: 294 B After Width: | Height: | Size: 294 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 323 B After Width: | Height: | Size: 316 B |
|
Before Width: | Height: | Size: 393 B After Width: | Height: | Size: 360 B |
|
Before Width: | Height: | Size: 393 B After Width: | Height: | Size: 360 B |
|
Before Width: | Height: | Size: 392 B After Width: | Height: | Size: 360 B |
|
Before Width: | Height: | Size: 390 B After Width: | Height: | Size: 360 B |
|
Before Width: | Height: | Size: 371 B After Width: | Height: | Size: 350 B |
|
Before Width: | Height: | Size: 375 B After Width: | Height: | Size: 350 B |
|
Before Width: | Height: | Size: 323 B After Width: | Height: | Size: 316 B |
|
Before Width: | Height: | Size: 323 B After Width: | Height: | Size: 316 B |
|
Before Width: | Height: | Size: 294 B After Width: | Height: | Size: 294 B |
|
Before Width: | Height: | Size: 412 B After Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 412 B After Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 412 B After Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 412 B After Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 575 B After Width: | Height: | Size: 577 B |
|
Before Width: | Height: | Size: 566 B After Width: | Height: | Size: 565 B |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 322 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 322 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 322 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 322 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 322 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 322 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 322 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 322 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 322 B After Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 820 B After Width: | Height: | Size: 761 B |
|
Before Width: | Height: | Size: 412 B After Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 412 B After Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 417 B After Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 408 B After Width: | Height: | Size: 353 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 405 B After Width: | Height: | Size: 322 B |
|
Before Width: | Height: | Size: 346 B After Width: | Height: | Size: 319 B |
|
Before Width: | Height: | Size: 405 B After Width: | Height: | Size: 322 B |
|
Before Width: | Height: | Size: 346 B After Width: | Height: | Size: 319 B |
|
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 379 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 379 B |
|
Before Width: | Height: | Size: 446 B After Width: | Height: | Size: 381 B |
|
Before Width: | Height: | Size: 449 B After Width: | Height: | Size: 376 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 882 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 426 B After Width: | Height: | Size: 420 B |
|
Before Width: | Height: | Size: 294 B After Width: | Height: | Size: 294 B |
|
Before Width: | Height: | Size: 294 B After Width: | Height: | Size: 294 B |
|
Before Width: | Height: | Size: 294 B After Width: | Height: | Size: 294 B |
|
Before Width: | Height: | Size: 415 B After Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 415 B After Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 415 B After Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 415 B After Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 415 B After Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 415 B After Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 294 B After Width: | Height: | Size: 294 B |
|
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 311 B |
|
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 311 B |
|
Before Width: | Height: | Size: 875 B After Width: | Height: | Size: 835 B |
|
Before Width: | Height: | Size: 812 B After Width: | Height: | Size: 793 B |
|
Before Width: | Height: | Size: 826 B After Width: | Height: | Size: 804 B |
|
Before Width: | Height: | Size: 819 B After Width: | Height: | Size: 804 B |
|
Before Width: | Height: | Size: 814 B After Width: | Height: | Size: 775 B |
|
Before Width: | Height: | Size: 787 B After Width: | Height: | Size: 777 B |
|
Before Width: | Height: | Size: 745 B After Width: | Height: | Size: 734 B |
|
Before Width: | Height: | Size: 772 B After Width: | Height: | Size: 747 B |
|
Before Width: | Height: | Size: 362 B After Width: | Height: | Size: 361 B |
@@ -1,49 +1,54 @@
|
||||
#!/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):
|
||||
Format (verifiziert gegen MENU.PCC/RAUSER1-3/FACTOR5, visuell + byte-exakt,
|
||||
UND jetzt zusaetzlich Pixel-fuer-Pixel gegen echte DOSBox-Screenshots von
|
||||
KARTE.PCC und KELLOGGS.PCC verglichen -- siehe Bugfix 2 unten):
|
||||
|
||||
- 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 /
|
||||
WICHTIG (Bugfix 1, 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).
|
||||
|
||||
- Byte 16 .. (len-769): RLE-komprimierte Pixel-Indexdaten.
|
||||
WICHTIG (Bugfix 2, 2026-07-22): Die RLE-Runs werden NICHT pro Scanline
|
||||
zurueckgesetzt/abgeschnitten. Frueher wurde nach `width` Pixeln pro Zeile
|
||||
hart getrimmt und der Rest eines laufenden RLE-Runs verworfen ("row reset").
|
||||
Das war FALSCH: dieses Format haelt sich nicht an die klassische PCX-Regel
|
||||
"ein Run ueberschreitet nie eine Scanline" -- Runs koennen frei ueber
|
||||
Zeilengrenzen hinweglaufen. Der Beweis: mit Row-Reset waren KARTE.PCC und
|
||||
KELLOGGS.PCC (die einzigen zwei echten 320x200-Vollbilder) sichtbar verwuerfelt
|
||||
(Charaktere an falscher Position, "Kerbe" im Logo-Rahmen), obwohl der
|
||||
Byte-Konsum fast vollstaendig war -- der Fehler kostet nur ~0.3-0.5% der
|
||||
Pixel, aber genau die falschen, wodurch ganze Bildbereiche sichtbar
|
||||
verrutschen. Mit kontinuierlicher Dekodierung (einfach `width*height` Pixel
|
||||
am Stueck aus dem RLE-Strom lesen, OHNE pro-Zeile zu trimmen) sind beide
|
||||
Bilder jetzt Pixel-fuer-Pixel identisch zu echten DOSBox-Screenshots
|
||||
(verifiziert per ImageMagick-Vergleich, siehe NOTES.md). Kleine Sprites
|
||||
waren von diesem Bug kaum betroffen, weil sie selten/nie einen Run ueber
|
||||
eine Zeilengrenze hinweg haben -- deshalb fiel es dort nicht auf.
|
||||
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%).
|
||||
GELOEST (frueher "bekannter offener Bug", Stand vor 2026-07-22 Nachmittag):
|
||||
Das "Kerbe"/Notch-Artefakt neben dem Kellogg's-Schriftzug und die verrutschten
|
||||
Charaktere auf KARTE.PCC waren beide der gleiche Bug (Row-Reset, s.o.), NICHT
|
||||
ein horizontales Rollen und NICHT eine Palette-Fehlzuordnung. Mit der
|
||||
kontinuierlichen Dekodierung ist das Artefakt komplett weg.
|
||||
|
||||
Nutzung: python3 pcc_to_png.py <input.PCC> <output.png>
|
||||
Schreibt ein PPM und konvertiert via ImageMagick `convert` zu PNG.
|
||||
Schreibt ein PPM und konvertiert via ImageMagick `convert`/`magick` zu PNG.
|
||||
"""
|
||||
import sys, struct, subprocess, os
|
||||
import sys, struct, subprocess, os, shutil
|
||||
|
||||
def decode_pcc(data):
|
||||
manuf, version, encoding, bpp = data[0], data[1], data[2], data[3]
|
||||
@@ -55,25 +60,23 @@ def decode_pcc(data):
|
||||
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)]
|
||||
|
||||
total = width * height
|
||||
out = bytearray()
|
||||
i = 0
|
||||
n = len(pixel_region)
|
||||
for row in range(height):
|
||||
row_out = bytearray()
|
||||
while len(row_out) < width and i < n:
|
||||
while len(out) < total 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)
|
||||
remaining = total - len(out)
|
||||
out.extend([val] * min(count, remaining))
|
||||
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)
|
||||
out.append(b)
|
||||
if len(out) < total:
|
||||
out.extend([0] * (total - len(out)))
|
||||
|
||||
return dict(width=width, height=height, marker_ok=(marker == 0x0C),
|
||||
pixels=bytes(out), palette=palette,
|
||||
@@ -88,7 +91,9 @@ def write_png(pixels, palette, width, height, out_png):
|
||||
r, g, b = palette[px]
|
||||
buf.extend([r, g, b])
|
||||
f.write(bytes(buf))
|
||||
subprocess.run(['convert', ppm, out_png], check=True)
|
||||
convert_bin = shutil.which('magick') or shutil.which('convert')
|
||||
args = [convert_bin, ppm, out_png] if 'magick' not in (convert_bin or '') or convert_bin.endswith('convert') else [convert_bin, 'convert', ppm, out_png]
|
||||
subprocess.run(args, check=True)
|
||||
os.remove(ppm)
|
||||
|
||||
def main():
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import os
|
||||
|
||||
base = "/home/aria/kellogg_remake/extracted_pre/"
|
||||
names = ["A","B","C","D","E","F","G","H","I","J","K","L","N","O","BLUME","WOLKE",
|
||||
"VITAMIN","COCOPOPS","SMACKIES","FROSTIES","LOOPS","TONY","SMACKS","TOUCAN",
|
||||
"COCO","PLATFW1","KEYS","GAMEBAR","KROKO","BOSSNAKE","DRAGON","DOORS"]
|
||||
|
||||
N = 128
|
||||
headers = {}
|
||||
for nm in names:
|
||||
path = base+nm+".BOB"
|
||||
data = open(path,"rb").read()
|
||||
headers[nm] = data[:N]
|
||||
print(f"{nm:10s} size={len(data):6d} header={data[:N].hex()}")
|
||||
|
||||
print()
|
||||
print("Per-byte-position constant analysis (position: set of distinct values, only show non-constant or show const marker):")
|
||||
for pos in range(N):
|
||||
vals = set(h[pos] for h in headers.values() if len(h) > pos)
|
||||
if len(vals) == 1:
|
||||
print(f"pos {pos:3d}: CONST 0x{next(iter(vals)):02x}")
|
||||
else:
|
||||
print(f"pos {pos:3d}: VARIES ({len(vals)} distinct)")
|
||||
@@ -0,0 +1,61 @@
|
||||
import struct
|
||||
|
||||
def decode_scanline(data, width, max_rows=100000):
|
||||
i = 0
|
||||
n = len(data)
|
||||
rows = 0
|
||||
row_len = 0
|
||||
while i < n:
|
||||
if row_len >= width:
|
||||
rows += 1
|
||||
row_len = 0
|
||||
if rows > max_rows:
|
||||
return rows, i, False
|
||||
continue
|
||||
b = data[i]
|
||||
if (b & 0xC0) == 0xC0:
|
||||
count = b & 0x3F
|
||||
i += 1
|
||||
if i >= n:
|
||||
return rows, i, False
|
||||
i += 1
|
||||
need = width - row_len
|
||||
take = min(count, need)
|
||||
row_len += take
|
||||
else:
|
||||
row_len += 1
|
||||
i += 1
|
||||
if row_len == 0:
|
||||
return rows, i, True
|
||||
else:
|
||||
rows += 1 # partial
|
||||
return rows, i, False
|
||||
|
||||
base = "/home/aria/kellogg_remake/extracted_pre/"
|
||||
files = ["B.BOB","E.BOB","N.BOB","O.BOB","A.BOB","J.BOB","K.BOB","D.BOB"]
|
||||
|
||||
for fn in files:
|
||||
data = open(base+fn, "rb").read()
|
||||
n = len(data)
|
||||
print(f"=== {fn} size={n} ===")
|
||||
found_any = False
|
||||
for headerlen in range(0, 17, 1):
|
||||
if headerlen % 2 != 0:
|
||||
continue
|
||||
header = data[:headerlen]
|
||||
body = data[headerlen:]
|
||||
num_u16 = headerlen // 2
|
||||
u16s = struct.unpack(f"<{num_u16}H", header) if headerlen else ()
|
||||
# try every pair (w_idx, h_idx) among header u16 fields as (width,height) candidates
|
||||
for wi in range(num_u16):
|
||||
w = u16s[wi]
|
||||
if w < 2 or w > 400:
|
||||
continue
|
||||
rows, consumed, ok = decode_scanline(body, w)
|
||||
if ok:
|
||||
# check if any header u16 equals rows (=height)
|
||||
match_h = [hi for hi,val in enumerate(u16s) if val == rows]
|
||||
print(f" headerlen={headerlen} width_idx={wi} width={w} -> rows={rows} clean=True header_u16={u16s} height_field_match_idx={match_h}")
|
||||
found_any = True
|
||||
if not found_any:
|
||||
print(" (no clean header+width combo found in range)")
|
||||
@@ -0,0 +1,30 @@
|
||||
def rle_decode_pcx_style(data):
|
||||
out = bytearray()
|
||||
i = 0
|
||||
n = len(data)
|
||||
while i < n:
|
||||
b = data[i]
|
||||
if (b & 0xC0) == 0xC0:
|
||||
count = b & 0x3F
|
||||
i += 1
|
||||
if i >= n:
|
||||
break
|
||||
val = data[i]
|
||||
out.extend([val]*count)
|
||||
i += 1
|
||||
else:
|
||||
out.append(b)
|
||||
i += 1
|
||||
return bytes(out)
|
||||
|
||||
base = "/home/aria/kellogg_remake/extracted_pre/"
|
||||
for fn in ["TONY.BOB","O.BOB","A.BOB"]:
|
||||
data = open(base+fn,"rb").read()
|
||||
dec = rle_decode_pcx_style(data)
|
||||
from collections import Counter
|
||||
c = Counter(dec)
|
||||
print(fn, "distinct values:", len(c), "min", min(dec), "max", max(dec))
|
||||
top = c.most_common(10)
|
||||
print(" top10:", top)
|
||||
below16 = sum(v for k,v in c.items() if k<16)
|
||||
print(f" fraction of pixels <16: {below16/len(dec):.3f} total_pixels={len(dec)}")
|
||||
@@ -0,0 +1,15 @@
|
||||
import os
|
||||
|
||||
base = "/home/aria/kellogg_remake/extracted_pre/"
|
||||
names = ["A","B","C","D","E","F","G","H","I","J","K","L","N","O","BLUME","WOLKE",
|
||||
"VITAMIN","COCOPOPS","SMACKIES","FROSTIES","LOOPS","TONY","SMACKS","TOUCAN",
|
||||
"COCO","PLATFW1","KEYS","GAMEBAR","KROKO","BOSSNAKE","DRAGON","DOORS"]
|
||||
|
||||
landmark = bytes.fromhex("7682b43a9f068882")
|
||||
|
||||
for nm in names:
|
||||
path = base+nm+".BOB"
|
||||
data = open(path,"rb").read()
|
||||
idx = data.find(landmark, 0, 64)
|
||||
pre = data[:idx] if idx>=0 else None
|
||||
print(f"{nm:10s} size={len(data):6d} landmark_at={idx:3d} preamble={pre.hex() if pre else 'NOTFOUND'}")
|
||||
@@ -0,0 +1,51 @@
|
||||
from PIL import Image
|
||||
import math
|
||||
|
||||
def rle_decode_pcx_style(data):
|
||||
out = bytearray()
|
||||
i = 0
|
||||
n = len(data)
|
||||
while i < n:
|
||||
b = data[i]
|
||||
if (b & 0xC0) == 0xC0:
|
||||
count = b & 0x3F
|
||||
i += 1
|
||||
if i >= n:
|
||||
break
|
||||
val = data[i]
|
||||
out.extend([val]*count)
|
||||
i += 1
|
||||
else:
|
||||
out.append(b)
|
||||
i += 1
|
||||
return bytes(out)
|
||||
|
||||
base = "/home/aria/kellogg_remake/extracted_pre/"
|
||||
fn = "O.BOB"
|
||||
data = open(base+fn, "rb").read()
|
||||
dec = rle_decode_pcx_style(data)
|
||||
print("decoded len:", len(dec))
|
||||
|
||||
widths = list(range(8, 60, 2))
|
||||
cell_h = 80
|
||||
cell_w = 60
|
||||
cols = 8
|
||||
rows = math.ceil(len(widths)/cols)
|
||||
sheet = Image.new("L", (cols*cell_w, rows*cell_h), 40)
|
||||
|
||||
for idx, w in enumerate(widths):
|
||||
h = (len(dec) + w - 1)//w
|
||||
padded = dec + bytes(w*h - len(dec))
|
||||
img = Image.frombytes("L", (w, h), padded)
|
||||
# scale to fit cell, keep aspect
|
||||
scale = min(cell_w/w, cell_h/h)
|
||||
nw, nh = max(1,int(w*scale)), max(1,int(h*scale))
|
||||
img_r = img.resize((nw, nh), Image.NEAREST)
|
||||
cx = (idx % cols) * cell_w
|
||||
cy = (idx // cols) * cell_h
|
||||
sheet.paste(img_r, (cx, cy))
|
||||
|
||||
sheet = sheet.resize((sheet.width*3, sheet.height*3), Image.NEAREST)
|
||||
outpath = "/home/aria/kellogg_remake/tools/o_bob_grid.png"
|
||||
sheet.save(outpath)
|
||||
print("saved", outpath, "widths:", widths)
|
||||
@@ -0,0 +1,66 @@
|
||||
from PIL import Image
|
||||
import math, sys
|
||||
|
||||
def rle_decode_scanline_clipped(data, width, max_rows=400):
|
||||
"""Decode with PCX-style per-scanline clipping: exactly `width` px per row,
|
||||
overshoot from a run is discarded, continues to next row."""
|
||||
rows = []
|
||||
row = []
|
||||
i = 0
|
||||
n = len(data)
|
||||
while i < n and len(rows) < max_rows:
|
||||
if len(row) >= width:
|
||||
rows.append(row)
|
||||
row = []
|
||||
continue
|
||||
b = data[i]
|
||||
if (b & 0xC0) == 0xC0:
|
||||
count = b & 0x3F
|
||||
i += 1
|
||||
if i >= n:
|
||||
break
|
||||
val = data[i]
|
||||
i += 1
|
||||
need = width - len(row)
|
||||
take = min(count, need)
|
||||
row.extend([val]*take)
|
||||
else:
|
||||
row.append(b)
|
||||
i += 1
|
||||
if row:
|
||||
row.extend([0]*(width-len(row)))
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
base = "/home/aria/kellogg_remake/extracted_pre/"
|
||||
fn = sys.argv[1] if len(sys.argv) > 1 else "O.BOB"
|
||||
skip = int(sys.argv[2]) if len(sys.argv) > 2 else 0
|
||||
data = open(base+fn, "rb").read()[skip:]
|
||||
|
||||
widths = list(range(8, 60, 2))
|
||||
cell_h = 90
|
||||
cell_w = 60
|
||||
cols = 8
|
||||
rows_n = math.ceil(len(widths)/cols)
|
||||
sheet = Image.new("L", (cols*cell_w, rows_n*cell_h), 40)
|
||||
|
||||
for idx, w in enumerate(widths):
|
||||
rows = rle_decode_scanline_clipped(data, w, max_rows=cell_h)
|
||||
h = len(rows)
|
||||
if h == 0:
|
||||
continue
|
||||
flat = bytearray()
|
||||
for r in rows:
|
||||
flat.extend(r)
|
||||
img = Image.frombytes("L", (w, h), bytes(flat))
|
||||
scale = min(cell_w/w, cell_h/h)
|
||||
nw, nh = max(1,int(w*scale)), max(1,int(h*scale))
|
||||
img_r = img.resize((nw, nh), Image.NEAREST)
|
||||
cx = (idx % cols) * cell_w
|
||||
cy = (idx // cols) * cell_h
|
||||
sheet.paste(img_r, (cx, cy))
|
||||
|
||||
sheet = sheet.resize((sheet.width*3, sheet.height*3), Image.NEAREST)
|
||||
outpath = f"/home/aria/kellogg_remake/tools/{fn}_grid_clip_skip{skip}.png"
|
||||
sheet.save(outpath)
|
||||
print("saved", outpath, "widths:", widths)
|
||||
@@ -0,0 +1,40 @@
|
||||
import sys, struct
|
||||
|
||||
def rle_decode_pcx_style(data):
|
||||
"""Decode assuming classic PCX RLE over the whole byte stream (no scanline boundary)."""
|
||||
out = bytearray()
|
||||
i = 0
|
||||
n = len(data)
|
||||
while i < n:
|
||||
b = data[i]
|
||||
if (b & 0xC0) == 0xC0:
|
||||
count = b & 0x3F
|
||||
i += 1
|
||||
if i >= n:
|
||||
break
|
||||
val = data[i]
|
||||
out.extend([val]*count)
|
||||
i += 1
|
||||
else:
|
||||
out.append(b)
|
||||
i += 1
|
||||
return bytes(out)
|
||||
|
||||
def factor_pairs_near_sqrt(total, tol=0):
|
||||
import math
|
||||
res = []
|
||||
for w in range(1, int(math.isqrt(total))+50):
|
||||
if w == 0: continue
|
||||
if total % w == 0:
|
||||
h = total // w
|
||||
res.append((w,h))
|
||||
return res
|
||||
|
||||
files = ["A.BOB","B.BOB","E.BOB","N.BOB","O.BOB","D.BOB","H.BOB","I.BOB","J.BOB","K.BOB","TONY.BOB"]
|
||||
base = "/home/aria/kellogg_remake/extracted_pre/"
|
||||
|
||||
for fn in files:
|
||||
path = base+fn
|
||||
data = open(path,"rb").read()
|
||||
dec = rle_decode_pcx_style(data)
|
||||
print(fn, "raw_len=",len(data), "decoded_len=",len(dec))
|
||||
@@ -0,0 +1,64 @@
|
||||
import sys
|
||||
|
||||
def try_decode_scanline(data, width, max_height=2000):
|
||||
"""PCX-style per-scanline RLE decode: for each row, decode exactly `width`
|
||||
pixels (excess from a run discarded), continue until input consumed.
|
||||
Returns (rows_pixels_list, bytes_consumed, ok, error_msg)."""
|
||||
i = 0
|
||||
n = len(data)
|
||||
rows = []
|
||||
row = []
|
||||
while i < n:
|
||||
if len(row) >= width:
|
||||
rows.append(row)
|
||||
row = []
|
||||
if len(rows) > max_height:
|
||||
return rows, i, False, "too many rows"
|
||||
continue
|
||||
b = data[i]
|
||||
if (b & 0xC0) == 0xC0:
|
||||
count = b & 0x3F
|
||||
i += 1
|
||||
if i >= n:
|
||||
return rows, i, False, "truncated run (missing value byte)"
|
||||
val = data[i]
|
||||
i += 1
|
||||
need = width - len(row)
|
||||
take = min(count, need)
|
||||
row.extend([val]*take)
|
||||
# NOTE: if count > need, the overshoot pixels for the *next* row
|
||||
# are DISCARDED per classic PCX semantics (not carried over)
|
||||
else:
|
||||
row.append(b)
|
||||
i += 1
|
||||
if row:
|
||||
# leftover partial row at EOF
|
||||
rows.append(row)
|
||||
return rows, i, False, f"EOF with partial row of {len(row)} px"
|
||||
return rows, i, True, "clean"
|
||||
|
||||
def scan_widths(data, wmin=4, wmax=250):
|
||||
results = []
|
||||
for w in range(wmin, wmax+1):
|
||||
rows, consumed, ok, msg = try_decode_scanline(data, w)
|
||||
# figure out leftover partial-row size from msg if present
|
||||
leftover = None
|
||||
if "partial row of" in msg:
|
||||
leftover = int(msg.split("partial row of")[1].split("px")[0].strip())
|
||||
elif msg == "clean":
|
||||
leftover = 0
|
||||
results.append((w, len(rows), leftover, ok, msg))
|
||||
return results
|
||||
|
||||
base = "/home/aria/kellogg_remake/extracted_pre/"
|
||||
files = ["A.BOB","B.BOB","E.BOB","N.BOB","O.BOB","D.BOB","J.BOB"]
|
||||
for fn in files:
|
||||
data = open(base+fn,"rb").read()
|
||||
print(f"=== {fn} (size={len(data)}) ===")
|
||||
results = scan_widths(data)
|
||||
clean = [r for r in results if r[2]==0]
|
||||
if clean:
|
||||
for r in clean:
|
||||
print(" CLEAN (leftover=0):", r)
|
||||
else:
|
||||
print(" no width in range gives leftover==0")
|
||||
@@ -0,0 +1,29 @@
|
||||
import struct, sys
|
||||
|
||||
path = "/home/aria/kellogg_remake/raw/PCKELL.DAT"
|
||||
data = open(path, "rb").read()
|
||||
print("total size:", len(data))
|
||||
|
||||
# Read uint16 LE values until they stop being monotonically non-decreasing
|
||||
vals = []
|
||||
off = 0
|
||||
prev = -1
|
||||
while off + 2 <= len(data):
|
||||
v = struct.unpack_from("<H", data, off)[0]
|
||||
vals.append(v)
|
||||
off += 2
|
||||
if len(vals) > 5000:
|
||||
break
|
||||
|
||||
# find longest monotonic non-decreasing prefix
|
||||
mono_len = 1
|
||||
for i in range(1, len(vals)):
|
||||
if vals[i] >= vals[i-1]:
|
||||
mono_len = i+1
|
||||
else:
|
||||
break
|
||||
|
||||
print("monotonic non-decreasing prefix length (uint16 entries):", mono_len)
|
||||
print("that's byte offset:", mono_len*2)
|
||||
print("first 40 vals:", vals[:40])
|
||||
print("vals around break point:", vals[max(0,mono_len-10):mono_len+10])
|
||||
@@ -0,0 +1,24 @@
|
||||
import struct
|
||||
|
||||
path = "/home/aria/kellogg_remake/raw/PCKELL.DAT"
|
||||
data = open(path, "rb").read()
|
||||
|
||||
vals = []
|
||||
off = 0
|
||||
while off + 2 <= len(data):
|
||||
v = struct.unpack_from("<H", data, off)[0]
|
||||
vals.append(v)
|
||||
off += 2
|
||||
|
||||
start = 6 # skip first 6 uint16 (12 bytes) which look like a header
|
||||
mono_len = start+1
|
||||
for i in range(start+1, len(vals)):
|
||||
if vals[i] >= vals[i-1]:
|
||||
mono_len = i+1
|
||||
else:
|
||||
break
|
||||
|
||||
print("monotonic non-decreasing run starting at idx", start, "-> length", mono_len-start, "entries")
|
||||
print("breaks at idx", mono_len, "byte offset", mono_len*2)
|
||||
print("values around break:", vals[mono_len-5:mono_len+15])
|
||||
print("last mono value:", vals[mono_len-1])
|
||||
@@ -0,0 +1,12 @@
|
||||
import struct
|
||||
|
||||
path = "/home/aria/kellogg_remake/raw/PCKELL.DAT"
|
||||
data = open(path, "rb").read()
|
||||
|
||||
# look at region from 580 to 900, print as hex with offsets
|
||||
start = 580
|
||||
end = 900
|
||||
for i in range(start, end, 16):
|
||||
chunk = data[i:i+16]
|
||||
hexs = ' '.join(f'{b:02x}' for b in chunk)
|
||||
print(f'{i:06x}: {hexs}')
|
||||
@@ -0,0 +1,16 @@
|
||||
import struct
|
||||
|
||||
path = "/home/aria/kellogg_remake/raw/PCKELL.DAT"
|
||||
data = open(path, "rb").read()
|
||||
|
||||
start = 588
|
||||
end = 900
|
||||
vals = []
|
||||
off = start
|
||||
while off+2 <= end:
|
||||
v = struct.unpack_from('<H', data, off)[0]
|
||||
vals.append((off, v))
|
||||
off += 2
|
||||
|
||||
for off, v in vals:
|
||||
print(off, hex(v), v)
|
||||