diff --git a/NOTES.md b/NOTES.md index 1f3802b..1e4c669 100644 --- a/NOTES.md +++ b/NOTES.md @@ -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). diff --git a/png_out/ANTS.png b/png_out/ANTS.png index 3901246..682c58a 100644 Binary files a/png_out/ANTS.png and b/png_out/ANTS.png differ diff --git a/png_out/CHOCOS.png b/png_out/CHOCOS.png index 6d2b403..aeb2f4f 100644 Binary files a/png_out/CHOCOS.png and b/png_out/CHOCOS.png differ diff --git a/png_out/CHOCOS10.png b/png_out/CHOCOS10.png index eacabfa..2095dd4 100644 Binary files a/png_out/CHOCOS10.png and b/png_out/CHOCOS10.png differ diff --git a/png_out/CHOCOS2.png b/png_out/CHOCOS2.png index f8a3730..6328f64 100644 Binary files a/png_out/CHOCOS2.png and b/png_out/CHOCOS2.png differ diff --git a/png_out/CHOCOS3.png b/png_out/CHOCOS3.png index f8a3730..6328f64 100644 Binary files a/png_out/CHOCOS3.png and b/png_out/CHOCOS3.png differ diff --git a/png_out/CHOCOS4.png b/png_out/CHOCOS4.png index 874bd41..6328f64 100644 Binary files a/png_out/CHOCOS4.png and b/png_out/CHOCOS4.png differ diff --git a/png_out/CHOCOS5.png b/png_out/CHOCOS5.png index 3ff1872..6328f64 100644 Binary files a/png_out/CHOCOS5.png and b/png_out/CHOCOS5.png differ diff --git a/png_out/CHOCOS6.png b/png_out/CHOCOS6.png index 8752848..2e00fcb 100644 Binary files a/png_out/CHOCOS6.png and b/png_out/CHOCOS6.png differ diff --git a/png_out/CHOCOS7.png b/png_out/CHOCOS7.png index 194533a..2e00fcb 100644 Binary files a/png_out/CHOCOS7.png and b/png_out/CHOCOS7.png differ diff --git a/png_out/CHOCOS8.png b/png_out/CHOCOS8.png index eacabfa..2095dd4 100644 Binary files a/png_out/CHOCOS8.png and b/png_out/CHOCOS8.png differ diff --git a/png_out/CHOCOS9.png b/png_out/CHOCOS9.png index eacabfa..2095dd4 100644 Binary files a/png_out/CHOCOS9.png and b/png_out/CHOCOS9.png differ diff --git a/png_out/COCO.png b/png_out/COCO.png index 3901246..682c58a 100644 Binary files a/png_out/COCO.png and b/png_out/COCO.png differ diff --git a/png_out/FAC0.png b/png_out/FAC0.png index 7a374ee..171c3d3 100644 Binary files a/png_out/FAC0.png and b/png_out/FAC0.png differ diff --git a/png_out/FAC1.png b/png_out/FAC1.png index af0f07d..171c3d3 100644 Binary files a/png_out/FAC1.png and b/png_out/FAC1.png differ diff --git a/png_out/FAC2.png b/png_out/FAC2.png index 7a374ee..171c3d3 100644 Binary files a/png_out/FAC2.png and b/png_out/FAC2.png differ diff --git a/png_out/FAC3.png b/png_out/FAC3.png index 7a374ee..171c3d3 100644 Binary files a/png_out/FAC3.png and b/png_out/FAC3.png differ diff --git a/png_out/FACTOR5.png b/png_out/FACTOR5.png index 0b58052..5337990 100644 Binary files a/png_out/FACTOR5.png and b/png_out/FACTOR5.png differ diff --git a/png_out/FON_1.png b/png_out/FON_1.png index cce01a6..a4388bc 100644 Binary files a/png_out/FON_1.png and b/png_out/FON_1.png differ diff --git a/png_out/FON_1B.png b/png_out/FON_1B.png index 11b9ad0..9ec9de9 100644 Binary files a/png_out/FON_1B.png and b/png_out/FON_1B.png differ diff --git a/png_out/FON_2.png b/png_out/FON_2.png index 8db6da4..a75a851 100644 Binary files a/png_out/FON_2.png and b/png_out/FON_2.png differ diff --git a/png_out/FRA_L.png b/png_out/FRA_L.png index c194e14..e7d06f8 100644 Binary files a/png_out/FRA_L.png and b/png_out/FRA_L.png differ diff --git a/png_out/FRA_LO.png b/png_out/FRA_LO.png index c194e14..e7d06f8 100644 Binary files a/png_out/FRA_LO.png and b/png_out/FRA_LO.png differ diff --git a/png_out/FRA_LU.png b/png_out/FRA_LU.png index c194e14..e7d06f8 100644 Binary files a/png_out/FRA_LU.png and b/png_out/FRA_LU.png differ diff --git a/png_out/FRA_M.png b/png_out/FRA_M.png index c194e14..e7d06f8 100644 Binary files a/png_out/FRA_M.png and b/png_out/FRA_M.png differ diff --git a/png_out/FRA_O.png b/png_out/FRA_O.png index c194e14..e7d06f8 100644 Binary files a/png_out/FRA_O.png and b/png_out/FRA_O.png differ diff --git a/png_out/FRA_R.png b/png_out/FRA_R.png index c194e14..e7d06f8 100644 Binary files a/png_out/FRA_R.png and b/png_out/FRA_R.png differ diff --git a/png_out/FRA_RO.png b/png_out/FRA_RO.png index c194e14..e7d06f8 100644 Binary files a/png_out/FRA_RO.png and b/png_out/FRA_RO.png differ diff --git a/png_out/FRA_RU.png b/png_out/FRA_RU.png index c194e14..e7d06f8 100644 Binary files a/png_out/FRA_RU.png and b/png_out/FRA_RU.png differ diff --git a/png_out/FRA_U.png b/png_out/FRA_U.png index c194e14..e7d06f8 100644 Binary files a/png_out/FRA_U.png and b/png_out/FRA_U.png differ diff --git a/png_out/GAMEBAR.png b/png_out/GAMEBAR.png index be0249d..847626b 100644 Binary files a/png_out/GAMEBAR.png and b/png_out/GAMEBAR.png differ diff --git a/png_out/ITE0.png b/png_out/ITE0.png index e1d722e..171c3d3 100644 Binary files a/png_out/ITE0.png and b/png_out/ITE0.png differ diff --git a/png_out/ITE1.png b/png_out/ITE1.png index e1d722e..171c3d3 100644 Binary files a/png_out/ITE1.png and b/png_out/ITE1.png differ diff --git a/png_out/ITE2.png b/png_out/ITE2.png index 6514edc..fdff446 100644 Binary files a/png_out/ITE2.png and b/png_out/ITE2.png differ diff --git a/png_out/ITE3.png b/png_out/ITE3.png index 5c98941..b40675c 100644 Binary files a/png_out/ITE3.png and b/png_out/ITE3.png differ diff --git a/png_out/KARTE.png b/png_out/KARTE.png index b2b8bea..0646fa7 100644 Binary files a/png_out/KARTE.png and b/png_out/KARTE.png differ diff --git a/png_out/KARTE0.png b/png_out/KARTE0.png index 22a4b88..fedcaba 100644 Binary files a/png_out/KARTE0.png and b/png_out/KARTE0.png differ diff --git a/png_out/KARTE1.png b/png_out/KARTE1.png index bfecb3f..475ada2 100644 Binary files a/png_out/KARTE1.png and b/png_out/KARTE1.png differ diff --git a/png_out/KARTE2.png b/png_out/KARTE2.png index 22a4b88..fedcaba 100644 Binary files a/png_out/KARTE2.png and b/png_out/KARTE2.png differ diff --git a/png_out/KARTE3.png b/png_out/KARTE3.png index bfecb3f..475ada2 100644 Binary files a/png_out/KARTE3.png and b/png_out/KARTE3.png differ diff --git a/png_out/KARTE4.png b/png_out/KARTE4.png index 68036e3..afae554 100644 Binary files a/png_out/KARTE4.png and b/png_out/KARTE4.png differ diff --git a/png_out/KARTE5.png b/png_out/KARTE5.png index fcbb095..afae554 100644 Binary files a/png_out/KARTE5.png and b/png_out/KARTE5.png differ diff --git a/png_out/KARTE6.png b/png_out/KARTE6.png index 2b6a668..5f24de2 100644 Binary files a/png_out/KARTE6.png and b/png_out/KARTE6.png differ diff --git a/png_out/KARTE7.png b/png_out/KARTE7.png index 17358be..fcbbc9f 100644 Binary files a/png_out/KARTE7.png and b/png_out/KARTE7.png differ diff --git a/png_out/KARTE8.png b/png_out/KARTE8.png index d1b62b0..019ca15 100644 Binary files a/png_out/KARTE8.png and b/png_out/KARTE8.png differ diff --git a/png_out/KARTE9.png b/png_out/KARTE9.png index 291f3ce..d4ced9f 100644 Binary files a/png_out/KARTE9.png and b/png_out/KARTE9.png differ diff --git a/png_out/KELL256A.png b/png_out/KELL256A.png index 18aea5a..04f1e6c 100644 Binary files a/png_out/KELL256A.png and b/png_out/KELL256A.png differ diff --git a/png_out/KELL256B.png b/png_out/KELL256B.png index d4481b3..341de95 100644 Binary files a/png_out/KELL256B.png and b/png_out/KELL256B.png differ diff --git a/png_out/KELL256C.png b/png_out/KELL256C.png index 836d366..9414552 100644 Binary files a/png_out/KELL256C.png and b/png_out/KELL256C.png differ diff --git a/png_out/KELL256D.png b/png_out/KELL256D.png index db327fd..02f041f 100644 Binary files a/png_out/KELL256D.png and b/png_out/KELL256D.png differ diff --git a/png_out/KELLOGGS.png b/png_out/KELLOGGS.png index fc581cb..d92f669 100644 Binary files a/png_out/KELLOGGS.png and b/png_out/KELLOGGS.png differ diff --git a/png_out/MENU.png b/png_out/MENU.png index d5ad31c..e28e418 100644 Binary files a/png_out/MENU.png and b/png_out/MENU.png differ diff --git a/png_out/PARTY1.png b/png_out/PARTY1.png index a5e313d..7908eec 100644 Binary files a/png_out/PARTY1.png and b/png_out/PARTY1.png differ diff --git a/png_out/PARTY2.png b/png_out/PARTY2.png index 7b5deeb..0365318 100644 Binary files a/png_out/PARTY2.png and b/png_out/PARTY2.png differ diff --git a/png_out/RAUSER1.png b/png_out/RAUSER1.png index f7682d4..ff78c99 100644 Binary files a/png_out/RAUSER1.png and b/png_out/RAUSER1.png differ diff --git a/png_out/RAUSER2.png b/png_out/RAUSER2.png index 24ef693..5ed2363 100644 Binary files a/png_out/RAUSER2.png and b/png_out/RAUSER2.png differ diff --git a/png_out/RAUSER3.png b/png_out/RAUSER3.png index c27dead..55e090f 100644 Binary files a/png_out/RAUSER3.png and b/png_out/RAUSER3.png differ diff --git a/png_out/SMACKS.png b/png_out/SMACKS.png index f425f71..9163634 100644 Binary files a/png_out/SMACKS.png and b/png_out/SMACKS.png differ diff --git a/png_out/TONY.png b/png_out/TONY.png index f425f71..57269d2 100644 Binary files a/png_out/TONY.png and b/png_out/TONY.png differ diff --git a/png_out/TOUCAN.png b/png_out/TOUCAN.png index dd55b2f..57269d2 100644 Binary files a/png_out/TOUCAN.png and b/png_out/TOUCAN.png differ diff --git a/png_out/VIT1.png b/png_out/VIT1.png index 1d2a2ac..0daf336 100644 Binary files a/png_out/VIT1.png and b/png_out/VIT1.png differ diff --git a/png_out/VIT2.png b/png_out/VIT2.png index 1d2a2ac..0daf336 100644 Binary files a/png_out/VIT2.png and b/png_out/VIT2.png differ diff --git a/png_out/VIT3.png b/png_out/VIT3.png index 1d2a2ac..0daf336 100644 Binary files a/png_out/VIT3.png and b/png_out/VIT3.png differ diff --git a/png_out/VIT4.png b/png_out/VIT4.png index 1d2a2ac..0daf336 100644 Binary files a/png_out/VIT4.png and b/png_out/VIT4.png differ diff --git a/png_out/VIT5.png b/png_out/VIT5.png index 1d2a2ac..0daf336 100644 Binary files a/png_out/VIT5.png and b/png_out/VIT5.png differ diff --git a/png_out/VIT6.png b/png_out/VIT6.png index 1d2a2ac..0daf336 100644 Binary files a/png_out/VIT6.png and b/png_out/VIT6.png differ diff --git a/png_out/W1.png b/png_out/W1.png index dd55b2f..57269d2 100644 Binary files a/png_out/W1.png and b/png_out/W1.png differ diff --git a/png_out/W2.png b/png_out/W2.png index 44a88cd..390b80a 100644 Binary files a/png_out/W2.png and b/png_out/W2.png differ diff --git a/png_out/W3.png b/png_out/W3.png index 73413be..2c06812 100644 Binary files a/png_out/W3.png and b/png_out/W3.png differ diff --git a/png_out/WECH_1.png b/png_out/WECH_1.png index f043381..c75b1e4 100644 Binary files a/png_out/WECH_1.png and b/png_out/WECH_1.png differ diff --git a/png_out/WECH_2.png b/png_out/WECH_2.png index 4554a7f..7cd9507 100644 Binary files a/png_out/WECH_2.png and b/png_out/WECH_2.png differ diff --git a/png_out/WECH_3.png b/png_out/WECH_3.png index 3c5ac0b..2061190 100644 Binary files a/png_out/WECH_3.png and b/png_out/WECH_3.png differ diff --git a/png_out/WECH_4.png b/png_out/WECH_4.png index ff69d5c..de7d2cf 100644 Binary files a/png_out/WECH_4.png and b/png_out/WECH_4.png differ diff --git a/png_out/WECH_5.png b/png_out/WECH_5.png index c10111a..8631c8e 100644 Binary files a/png_out/WECH_5.png and b/png_out/WECH_5.png differ diff --git a/png_out/WECH_6.png b/png_out/WECH_6.png index 253fc5b..a361202 100644 Binary files a/png_out/WECH_6.png and b/png_out/WECH_6.png differ diff --git a/png_out/WECH_7.png b/png_out/WECH_7.png index a224852..79ca3a5 100644 Binary files a/png_out/WECH_7.png and b/png_out/WECH_7.png differ diff --git a/png_out/WECH_8.png b/png_out/WECH_8.png index be1b2e8..71669d7 100644 Binary files a/png_out/WECH_8.png and b/png_out/WECH_8.png differ diff --git a/png_out/WECH_9.png b/png_out/WECH_9.png index f9d2894..d74e5bf 100644 Binary files a/png_out/WECH_9.png and b/png_out/WECH_9.png differ diff --git a/tools/pcc_to_png.py b/tools/pcc_to_png.py index 29f66e1..8030302 100644 --- a/tools/pcc_to_png.py +++ b/tools/pcc_to_png.py @@ -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 -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: - 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) + 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 + remaining = total - len(out) + out.extend([val] * min(count, remaining)) + else: + 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(): diff --git a/tools/wip_bob_sprite_research/bob_header_diff.py b/tools/wip_bob_sprite_research/bob_header_diff.py new file mode 100644 index 0000000..1611d97 --- /dev/null +++ b/tools/wip_bob_sprite_research/bob_header_diff.py @@ -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)") diff --git a/tools/wip_bob_sprite_research/bob_header_search.py b/tools/wip_bob_sprite_research/bob_header_search.py new file mode 100644 index 0000000..0b9367c --- /dev/null +++ b/tools/wip_bob_sprite_research/bob_header_search.py @@ -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)") diff --git a/tools/wip_bob_sprite_research/bob_histogram.py b/tools/wip_bob_sprite_research/bob_histogram.py new file mode 100644 index 0000000..b6bf003 --- /dev/null +++ b/tools/wip_bob_sprite_research/bob_histogram.py @@ -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)}") diff --git a/tools/wip_bob_sprite_research/bob_landmark.py b/tools/wip_bob_sprite_research/bob_landmark.py new file mode 100644 index 0000000..d77a8fe --- /dev/null +++ b/tools/wip_bob_sprite_research/bob_landmark.py @@ -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'}") diff --git a/tools/wip_bob_sprite_research/bob_render_grid.py b/tools/wip_bob_sprite_research/bob_render_grid.py new file mode 100644 index 0000000..de2787d --- /dev/null +++ b/tools/wip_bob_sprite_research/bob_render_grid.py @@ -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) diff --git a/tools/wip_bob_sprite_research/bob_render_grid2.py b/tools/wip_bob_sprite_research/bob_render_grid2.py new file mode 100644 index 0000000..60f9065 --- /dev/null +++ b/tools/wip_bob_sprite_research/bob_render_grid2.py @@ -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) diff --git a/tools/wip_bob_sprite_research/bob_rle_test.py b/tools/wip_bob_sprite_research/bob_rle_test.py new file mode 100644 index 0000000..af04dbc --- /dev/null +++ b/tools/wip_bob_sprite_research/bob_rle_test.py @@ -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)) diff --git a/tools/wip_bob_sprite_research/bob_width_search.py b/tools/wip_bob_sprite_research/bob_width_search.py new file mode 100644 index 0000000..f371a94 --- /dev/null +++ b/tools/wip_bob_sprite_research/bob_width_search.py @@ -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") diff --git a/tools/wip_bob_sprite_research/dat_analyze.py b/tools/wip_bob_sprite_research/dat_analyze.py new file mode 100644 index 0000000..090d26f --- /dev/null +++ b/tools/wip_bob_sprite_research/dat_analyze.py @@ -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(" 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]) diff --git a/tools/wip_bob_sprite_research/dat_analyze2.py b/tools/wip_bob_sprite_research/dat_analyze2.py new file mode 100644 index 0000000..b99edeb --- /dev/null +++ b/tools/wip_bob_sprite_research/dat_analyze2.py @@ -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("= 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]) diff --git a/tools/wip_bob_sprite_research/dat_table2.py b/tools/wip_bob_sprite_research/dat_table2.py new file mode 100644 index 0000000..e24765c --- /dev/null +++ b/tools/wip_bob_sprite_research/dat_table2.py @@ -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}') diff --git a/tools/wip_bob_sprite_research/dat_u16.py b/tools/wip_bob_sprite_research/dat_u16.py new file mode 100644 index 0000000..5a44623 --- /dev/null +++ b/tools/wip_bob_sprite_research/dat_u16.py @@ -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('