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)
This commit is contained in:
ARIA
2026-07-22 07:33:01 +00:00
parent 3c650e6aef
commit 5efba4b165
91 changed files with 561 additions and 43 deletions
+82
View File
@@ -465,3 +465,85 @@ Original-Asset-Material, nur unverpackt). Mit eingecheckt: `NOTES.md`,
`tools/` (unser Code: `split_pre.py`, `pcc_to_png.py`), `dosbox/` `tools/` (unser Code: `split_pre.py`, `pcc_to_png.py`), `dosbox/`
(Test-Konfiguration), `png_out/` (unsere konvertierten PNG-Ergebnisse, (Test-Konfiguration), `png_out/` (unsere konvertierten PNG-Ergebnisse,
408 KB, reine Arbeitsergebnisse kein Originalmaterial). 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).
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 B

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 B

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 B

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 B

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 B

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 375 B

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 B

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 B

After

Width:  |  Height:  |  Size: 316 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

After

Width:  |  Height:  |  Size: 294 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 B

After

Width:  |  Height:  |  Size: 355 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 B

After

Width:  |  Height:  |  Size: 355 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 B

After

Width:  |  Height:  |  Size: 355 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 B

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 575 B

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 566 B

After

Width:  |  Height:  |  Size: 565 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 325 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 325 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 325 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 325 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 820 B

After

Width:  |  Height:  |  Size: 761 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 B

After

Width:  |  Height:  |  Size: 355 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 B

After

Width:  |  Height:  |  Size: 355 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 B

After

Width:  |  Height:  |  Size: 358 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 B

After

Width:  |  Height:  |  Size: 353 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 B

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 B

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 465 B

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 446 B

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 B

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 B

After

Width:  |  Height:  |  Size: 420 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

After

Width:  |  Height:  |  Size: 294 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

After

Width:  |  Height:  |  Size: 294 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 355 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 355 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 355 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 355 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 355 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 355 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

After

Width:  |  Height:  |  Size: 294 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 B

After

Width:  |  Height:  |  Size: 311 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 B

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 875 B

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 812 B

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 826 B

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 819 B

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 814 B

After

Width:  |  Height:  |  Size: 775 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 787 B

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 745 B

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 772 B

After

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 B

After

Width:  |  Height:  |  Size: 361 B

+48 -43
View File
@@ -1,49 +1,54 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Finaler PCC-Decoder fuer Kellogg's Tony and Friends (2026-07-22, ARIA). """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, - 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). 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 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 Vollbild-Screens (320x200) zufaellig identisch aus und wurden erst dafuer
gehalten -- sind aber tatsaechlich NICHT die Bilddimensionen (vermutlich gehalten -- sind aber tatsaechlich NICHT die Bilddimensionen (vermutlich
ein DPI/Reserved-Feld wie im echten 128-Byte-PCX-Header), sondern ein ein DPI/Reserved-Feld wie im echten 128-Byte-PCX-Header), sondern ein
Konstantwert der bei kleinen Sprites/Logos (z.B. RAUSER1.PCC: 182x46, Konstantwert der bei kleinen Sprites/Logos (z.B. RAUSER1.PCC: 182x46,
FAC0.PCC: 16x16) komplett falsch war und zu kaputten/leeren Bildern fuehrte. 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 - Byte 16 .. (len-769): RLE-komprimierte Pixel-Indexdaten.
statt echten 199) zu ueberlaufen. WICHTIG (Bugfix 2, 2026-07-22): Die RLE-Runs werden NICHT pro Scanline
- Byte 16 .. (len-769): RLE-komprimierte Pixel-Indexdaten, SCANLINE-weise zurueckgesetzt/abgeschnitten. Frueher wurde nach `width` Pixeln pro Zeile
decodiert (pro Zeile wird bis width Pixel gefuellt, ueberschuessige Pixel hart getrimmt und der Rest eines laufenden RLE-Runs verworfen ("row reset").
eines Runs am Zeilenende werden verworfen -- klassisches PCX-Verhalten). 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), RLE-Tupel: Byte mit oberen 2 Bits gesetzt (0xC0-0xFF) = Lauflaenge (&0x3F),
gefolgt von einem Wert-Byte. Sonst literaler Pixel. gefolgt von einem Wert-Byte. Sonst literaler Pixel.
- Letzte 769 Bytes: 0x0C-Marker + 768 Byte (256 x RGB) eingebettete Palette - Letzte 769 Bytes: 0x0C-Marker + 768 Byte (256 x RGB) eingebettete Palette
(klassische PCX-v5-256-Farben-Erweiterung). PRO DATEI eigene Palette, (klassische PCX-v5-256-Farben-Erweiterung). PRO DATEI eigene Palette,
keine globale Palette noetig. keine globale Palette noetig.
BEKANNTER OFFENER BUG (Stand 2026-07-22, noch nicht geloest): GELOEST (frueher "bekannter offener Bug", Stand vor 2026-07-22 Nachmittag):
KELLOGGS.PCC und KARTE.PCC (volle 320x200-Screens) zeigen weiterhin einen Das "Kerbe"/Notch-Artefakt neben dem Kellogg's-Schriftzug und die verrutschten
lokalen Deko-Fehler (ein "Kerbe"/Notch-Artefakt rechts neben dem Kellogg's- Charaktere auf KARTE.PCC waren beide der gleiche Bug (Row-Reset, s.o.), NICHT
Schriftzug, roter/gelber Fleck der nicht ins Referenz-Screenshot passt). ein horizontales Rollen und NICHT eine Palette-Fehlzuordnung. Mit der
Ausgeschlossen als Ursache: (a) horizontales Rollen/Verschieben des ganzen kontinuierlichen Dekodierung ist das Artefakt komplett weg.
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> 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): def decode_pcc(data):
manuf, version, encoding, bpp = data[0], data[1], data[2], data[3] 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] 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)] palette = [(pal_bytes[i], pal_bytes[i+1], pal_bytes[i+2]) for i in range(0, 768, 3)]
total = width * height
out = bytearray() out = bytearray()
i = 0 i = 0
n = len(pixel_region) n = len(pixel_region)
for row in range(height): while len(out) < total and i < n:
row_out = bytearray() b = pixel_region[i]; i += 1
while len(row_out) < width and i < n: if (b & 0xC0) == 0xC0:
b = pixel_region[i]; i += 1 count = b & 0x3F
if (b & 0xC0) == 0xC0: if i >= n:
count = b & 0x3F break
if i >= n: val = pixel_region[i]; i += 1
break remaining = total - len(out)
val = pixel_region[i]; i += 1 out.extend([val] * min(count, remaining))
row_out.extend([val] * count) else:
else: out.append(b)
row_out.append(b) if len(out) < total:
row_out = row_out[:width] out.extend([0] * (total - len(out)))
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), return dict(width=width, height=height, marker_ok=(marker == 0x0C),
pixels=bytes(out), palette=palette, pixels=bytes(out), palette=palette,
@@ -88,7 +91,9 @@ def write_png(pixels, palette, width, height, out_png):
r, g, b = palette[px] r, g, b = palette[px]
buf.extend([r, g, b]) buf.extend([r, g, b])
f.write(bytes(buf)) 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) os.remove(ppm)
def main(): 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}')
+16
View File
@@ -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)