diff --git a/NOTES.md b/NOTES.md index 14121b7..84c9cb0 100644 --- a/NOTES.md +++ b/NOTES.md @@ -778,3 +778,114 @@ zu einer Sequenz zusammengesetzt):** geht und einfach hier weitergelesen werden kann. Laeuft jetzt selbstaendig weiter, keine Rueckfrage noetig. + +## Boot-Sequenz vermessen + Intro-Player gebaut (2026-07-22 abends, Fortsetzung) + +### 1) Echter Regressions-Bug gefunden + gefixt (tools/pcc_to_png.py) + +Beim Draufschauen fuer die Intro-Arbeit aufgefallen: Commit `62af15e` hatte +den PCC-Decoder korrekt auf ImageMagick umgestellt (nachdem zwei eigene +Hand-RLE-Parser nachweislich falsche Pixel lieferten). Der naechste Commit +`3410a53` (eigentlich nur fuer BOB/ICO/MAP gedacht) hat `pcc_to_png.py` +aber **versehentlich durch eine AELTERE Version mit dem kaputten +Hand-RLE-Decoder ueberschrieben** -- vermutlich ein Merge-/Copy-Fehler beim +Zusammenfuehren von Aenderungen. Die bereits committeten 77 PNGs in +`png_out/` waren zum Glueck NICHT betroffen (die stammen noch von der guten +ImageMagick-Version), aber ein Rerun des Skripts haette wieder kaputte +Bilder erzeugt, ohne dass es aufgefallen waere. + +**Fix:** Statt ImageMagick-Subprocess jetzt `kellogg_formats.load_pcc()` -- +nutzt Pillows eingebauten, robusten PCX-Decoder direkt (`PIL.Image.open` +erkennt PCX an den Magic-Bytes, nicht am Dateinamen). Kein eigener +RLE-Parser mehr im Projekt, an keiner Stelle. Alle 77 PCC neu aus +`PCKELL.DAT` gerendert und via `compare -metric AE` **byte-fuer-byte +pixelidentisch (AE=0)** zu den alten (guten) PNGs verifiziert -- keine +Regression, sauberer Fix. Neue Funktionen `load_pcc()` / `pcc_to_rgba()` +in `kellogg_formats.py`, wiederverwendbar fuer den Intro-Player. + +### 2) Boot-Sequenz per echter Screenshot-Serie vermessen (0.3s-Intervall) + +`xvfb-run`-Setup (Xvfb + `import -window root` alle 0.3s) waehrend +`START.EXE` bootet, ~90 Frames ueber ~27s. Ergebnis (Hash-Dedup + visuelle +Pruefung der Uebergaenge): + +1. **Rauser-Advertainment-Karte** -- Komposit aus `RAUSER1.PCC` (dunkle Box + "RAUSER"), `RAUSER3.PCC` (blaue Box, Ausrufezeichen), `RAUSER2.PCC` + ("ADVERTAINMENT"-Schriftzug) auf weissem Grund. Statisch gehalten (kein + Aufbau-Effekt beobachtet), ~3s. +2. Kurze schwarze Pause (~0.3-0.5s). +3. **Factor5-Studio-Logo** (`FACTOR5.PCC`, 320x199) -- **komplett statisches + Bild**, keine Laufzeit-Animation. Das "Geister"-Doppellogo (zwei leicht + versetzte FACTOR-Schriftzuege + Spiegelung am Boden) ist Teil des Assets + selbst (Byte-Konsum exakt 100%, kein Bug). Lange gehalten, ~6s. +4. Kurze schwarze Pause. +5. **Kellogg's-Markenlogo** (`KELLOGGS.PCC`, 320x200) -- erscheint SOFORT + komplett und bleibt die ganze Zeit scharf. **KORREKTUR einer fruehen + Fehlannahme:** es gibt HIER keinen Vorhang-/Wipe-Effekt auf dem Logo + selbst. Was tatsaechlich passiert: der Text **"praesentiert"** darunter + faedet per **Palette-Animation** von blass (fast weiss) zu kraeftigem + Rot ein (~3-4s beobachtet). Per Index-Analyse verifiziert: die Pixel des + Textes nutzen exklusiv die Palette-Indizes **34, 42, 43, 44, 45** (Bbox + x=71-241, y=108-141 im 320x200-Bild) -- alles andere im Bild bleibt + waehrend des Fades unveraendert. +6. Kurze schwarze Pause. +7. **Titelbild "TONY & FRIENDS in Kellogg's Land"** -- **DAS ist der + eigentliche Vorhang-Effekt**, den Stefan urspruenglich meinte. Wichtiger + Fund: dieses Bild ist intern **320x480** (doppelt so hoch wie die + anderen Screens!), zusammengesetzt aus den 4 PCC-Quadranten `KELL256A` + (oben links) / `KELL256B` (oben rechts) / `KELL256C` (unten links) / + `KELL256D` (unten rechts), je 160x240 -- durch Stitchen zu einem + 320x480-Gesamtbild verifiziert (siehe `/tmp/kell_full_stitch.png` + waehrend der Session, zeigt exakt das erwartete Titelbild). Der + Reveal-Effekt: **obere Bildhaelfte (A|B) faehrt von oben nach unten + ein, untere Haelfte (C|D) von unten nach oben** -- wie ein Theater- + vorhang, nur vertikal, beide Haelften treffen sich exakt in der Mitte + (y=240 im 480px-Bild). Dauer ~3s (Frames 73->83 in der Aufnahme). + DOSBox-Fenster ist fuer diesen Screen sichtbar groesser/anders + proportioniert als bei den 320x240-Screens davor -- passt zur These + eines eigenen 320x480-Modus nur fuer dieses Bild. + +Alle anderen Screens (Rauser/Factor5/Kellogg's-Logo) laufen im +Fenster-Seitenverhaeltnis 320x240 (aus Trim-Analyse des DOSBox-Fensters: +640x480 Xvfb-Ausschnitt = exakt 2x 320x240) -- die PCC-Bilder selbst sind +oft nur 320x199/200 und werden darin mit schwarzen Balken oben/unten +letterboxed (gut sichtbar am schwarzen Rand um die Kellogg's-Box in den +Screenshots). + +### 3) Erster spielbarer Meilenstein: `tools/intro_sequence.py` + +pygame-Player, der die komplette oben beschriebene Sequenz nachbaut: +Asset-Loading aus `PCKELL.DAT` (ueber `kellogg_formats`), Bild-Rendering +mit Palette, Timing/Sequenzer, Palette-Fade-Effekt (echte Index-Animation, +kein Alpha-Crossfade-Hack), Vorhang-Wipe-Effekt fuer das Titelbild. +Headless getestet (Xvfb + Screenshot-Serie) -- **visuell verifiziert, +sieht in allen Phasen sehr nah am echten DOSBox-Boot aus**: + +[FILE: /shared/uploads/aria_intro_rauser.png] (Rauser-Karte, Komposit) +[FILE: /shared/uploads/aria_intro_factor5.png] (Factor5-Logo, statisch) +[FILE: /shared/uploads/aria_intro_kelloggs_fade.png] ("praesentiert" mitten im Fade) +[FILE: /shared/uploads/aria_intro_titel_wipe.png] (Titelbild-Vorhang mitten im Reveal) +[FILE: /shared/uploads/aria_intro_titel_final.png] (Titelbild komplett offen) + +**Bekannte Ungenauigkeiten (v1, nicht pixelperfekt, aber nah dran):** +- Rauser-Komposit-Layout (Position von RAUSER1/2/3 zueinander) ist per + Augenmass an die Screenshots angenaehert, nicht pixelgenau vermessen -- + bei Bedarf spaeter per genauerem Screenshot-Vergleich nachjustieren. +- Fade-/Wipe-Dauern sind aus der 0.3s-Sample-Screenshot-Serie abgeleitet + (+/- ein paar hundert ms Unschaerfe), nicht aus dem Originalcode gelesen + (RTM.EXE ist compiliert, kein Quellcode verfuegbar). +- Sound fehlt komplett (TFMX/SAM noch nicht decodiert, siehe unten). + +**Ausfuehren:** `python3 tools/intro_sequence.py` auf `aria-wohnung` +(pygame jetzt per `apt install python3-pygame` installiert, NICHT pip -- +System-Python ist "externally managed", pip-User-Install schlug fehl). +`--once` fuer einen Durchlauf statt Endlosschleife wie im Original. + +### Naechste Schritte (Prioritaet) +1. Sound (SAM/TFX TFMX-Format) decodieren + an die Intro-Sequenz haengen -- + `tools/tfmx_player/` (C-Referenztool) liegt bereit. +2. Rauser-Layout und Fade/Wipe-Timings feinjustieren falls gewuenscht. +3. Danach: Uebergang von Intro zu echtem Gameplay -- Tony-Bewegung auf + Level W1L0 als naechster grosser Meilenstein (MAP+ICO+BOB sind fertig). + +Laeuft selbstaendig weiter, keine Rueckfrage noetig. diff --git a/tools/intro_sequence.py b/tools/intro_sequence.py new file mode 100644 index 0000000..c2f2e46 --- /dev/null +++ b/tools/intro_sequence.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +""" +Boot-/Intro-Sequenz von "Tony & Friends in Kellogg's Land" -- erster +spielbarer (bzw. hier: zuschaubarer) pygame-Meilenstein des Remakes. + +Reihenfolge + Timing wurde am 22.07.2026 durch eine echte DOSBox-Screenshot- +Serie (0.3s-Intervall) waehrend des Originalboots vermessen, siehe NOTES.md +Abschnitt "Boot-Sequenz vermessen". Kurzfassung: + + 1. Rauser-Advertainment-Karte (Komposit aus RAUSER1+2+3.PCC), ~3s + 2. kurze schwarze Pause + 3. Factor5-Studio-Logo (FACTOR5.PCC, STATISCHES Bild -- keine Animation, + das "Geister"-Doppellogo ist Teil des Assets selbst), ~6s + 4. kurze schwarze Pause + 5. Kellogg's-Markenlogo (KELLOGGS.PCC) erscheint SOFORT komplett; darunter + faedet der Text "praesentiert" per Palette-Fade von blass nach saettigt- + rot ein (~3s). WICHTIG: das ist NICHT der vom Stefan vermutete "Vorhang"- + Effekt auf dem Logo selbst -- das Logo ist die ganze Zeit scharf/komplett + da, nur der Text darunter faedet. + 6. kurze schwarze Pause + 7. Titelbild "TONY & FRIENDS in Kellogg's Land" -- DAS ist der eigentliche + Vorhang-Effekt: das Bild ist intern 320x480 (doppelt so hoch wie die + anderen 320x200/240-Screens!), zusammengesetzt aus den 4 Quadranten + KELL256A (oben links) / B (oben rechts) / C (unten links) / D (unten + rechts), je 160x240. Obere Haelfte (A|B) faehrt von oben nach unten ein, + untere Haelfte (C|D) von unten nach oben -- wie ein Theatervorhang, nur + vertikal, beide Haelften treffen sich in der Bildmitte. Dauer ~3s. + +Alle Nicht-BOB/ICO/MAP-Assets werden per kellogg_formats.load_pcc() (Pillows +PCX-Decoder) aus PCKELL.DAT gelesen -- kein eigener RLE-Code mehr (siehe +Regressions-Warnung in pcc_to_png.py). + +Rauser-Layout und Fade-Text-Bounding-Box sind aus den echten Screenshots per +Augenmass/Index-Analyse bestimmt (siehe tools/analyze_kelloggs.py-Fund: +Indizes 34/42/43/44/45 in KELLOGGS.PCC = "praesentiert"-Text, bbox x71-241 +y108-141) -- nicht 100% pixelidentisch zum Original, aber nah dran. Kann bei +Bedarf spaeter nachjustiert werden. + +Aufruf: python3 intro_sequence.py [--once] (--once: nach einem Durchlauf beenden, +sonst loopt die Sequenz weiter wie im Original-Titelbildschirm) +""" +import sys +import os +import time + +sys.path.insert(0, os.path.dirname(__file__)) +from kellogg_formats import DATContainer, load_pcc + +import pygame + +DAT_PATH = os.environ.get('KELLOGG_DAT', '/home/aria/kellogg_remake/raw/PCKELL.DAT') + +WINDOW_W, WINDOW_H = 960, 720 +BG_COLOR = (0, 0, 0) + +# "praesentiert"-Text-Faelle in KELLOGGS.PCC (siehe Docstring) +PRAESENTIERT_FADE_INDICES = [34, 42, 43, 44, 45] +PRAESENTIERT_FADE_SECONDS = 2.5 + + +def indices_to_surface(width, height, indices, palette): + '''Baut ein pygame.Surface (RGB) aus Palette-Indizes + (r,g,b)-Liste.''' + buf = bytearray(width * height * 3) + for i, idx in enumerate(indices): + r, g, b = palette[idx & 0xFF] + buf[i*3:i*3+3] = bytes([r, g, b]) + surf = pygame.image.frombuffer(bytes(buf), (width, height), 'RGB') + return surf.convert() + + +class Assets: + def __init__(self, dat_path): + self.container = DATContainer(dat_path) + + def pcc(self, name): + '''name z.B. "RAUSER1" (ohne .PCC). Gibt (w,h,indices,palette) zurueck.''' + return load_pcc(self.container.entries[name + '.PCC']) + + def pcc_surface(self, name): + w, h, indices, palette = self.pcc(name) + return indices_to_surface(w, h, indices, palette) + + +def fit_scale(src_w, src_h, max_w, max_h): + return min(max_w / src_w, max_h / src_h) + + +def blit_scaled_centered(screen, surface, window_w, window_h): + scale = fit_scale(surface.get_width(), surface.get_height(), window_w, window_h) + new_size = (max(1, int(surface.get_width() * scale)), max(1, int(surface.get_height() * scale))) + scaled = pygame.transform.scale(surface, new_size) + x = (window_w - new_size[0]) // 2 + y = (window_h - new_size[1]) // 2 + screen.fill(BG_COLOR) + screen.blit(scaled, (x, y)) + return scale, x, y + + +def wait_or_skip(clock, seconds, screen_update_fn, fps=30): + '''Laeuft seconds lang, ruft pro Frame screen_update_fn(t_norm 0..1) auf. + Bricht per ESC/Klick vorzeitig ab (gibt True zurueck wenn User quit will).''' + start = time.monotonic() + while True: + now = time.monotonic() + t = (now - start) / seconds if seconds > 0 else 1.0 + if t >= 1.0: + screen_update_fn(1.0) + pygame.display.flip() + return False + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return True + if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: + return True + screen_update_fn(t) + pygame.display.flip() + clock.tick(fps) + + +def build_rauser_composite(assets): + '''RAUSER1 (dunkle Box "RAUSER") + RAUSER3 (Ausrufezeichen, blaue Box) + + RAUSER2 ("ADVERTAINMENT"-Schriftzug) zu EINER Karte auf weissem Grund + zusammengesetzt (320x240 nativ, wie der Rest der Boot-Screens).''' + canvas = pygame.Surface((320, 240)) + canvas.fill((255, 255, 255)) + + r1 = assets.pcc_surface('RAUSER1') # 182x46 "RAUSER" + r2 = assets.pcc_surface('RAUSER2') # 228x21 "ADVERTAINMENT" + r3 = assets.pcc_surface('RAUSER3') # 16x55 "!" + + total_w = max(r1.get_width() + r3.get_width(), r2.get_width()) + x0 = (320 - total_w) // 2 + y0 = (240 - (r1.get_height() + r2.get_height())) // 2 + + canvas.blit(r1, (x0, y0)) + canvas.blit(r3, (x0 + r1.get_width(), y0 - (r3.get_height() - r1.get_height()) // 2)) + canvas.blit(r2, (x0, y0 + r1.get_height())) + return canvas + + +def build_letterboxed(surface, canvas_w=320, canvas_h=240): + '''Zentriert ein kleineres PCC-Bild (z.B. 320x199/200) auf eine 320x240- + Leinwand mit schwarzen Balken -- wie im echten DOSBox-Fenster beobachtet.''' + canvas = pygame.Surface((canvas_w, canvas_h)) + canvas.fill((0, 0, 0)) + x = (canvas_w - surface.get_width()) // 2 + y = (canvas_h - surface.get_height()) // 2 + canvas.blit(surface, (x, y)) + return canvas + + +def build_kelloggs_frame(w, h, indices, palette, fade_t): + '''fade_t: 0..1, faedet PRAESENTIERT_FADE_INDICES von blass (Richtung + Hintergrundweiss) zu ihrer echten Palettenfarbe.''' + dyn_pal = list(palette) + for idx in PRAESENTIERT_FADE_INDICES: + r, g, b = palette[idx] + # start: sehr blass (nah am weissen Logo-Hintergrund), Ende: echte Farbe + pale = (255, 245, 245) + nr = int(pale[0] + (r - pale[0]) * fade_t) + ng = int(pale[1] + (g - pale[1]) * fade_t) + nb = int(pale[2] + (b - pale[2]) * fade_t) + dyn_pal[idx] = (nr, ng, nb) + return indices_to_surface(w, h, indices, dyn_pal) + + +def build_title_image(assets): + '''Stitcht KELL256A/B (oben) + C/D (unten) zu einem 320x480-Gesamtbild.''' + a = assets.pcc_surface('KELL256A') + b = assets.pcc_surface('KELL256B') + c = assets.pcc_surface('KELL256C') + d = assets.pcc_surface('KELL256D') + w = a.get_width() + b.get_width() + h = a.get_height() + c.get_height() + canvas = pygame.Surface((w, h)) + canvas.blit(a, (0, 0)) + canvas.blit(b, (a.get_width(), 0)) + canvas.blit(c, (0, a.get_height())) + canvas.blit(d, (a.get_width(), a.get_height())) + return canvas, a.get_height() # (Gesamtbild, Hoehe der oberen Haelfte) + + +def run_once(screen, clock, assets): + quit_requested = False + + # 1) Rauser + rauser = build_rauser_composite(assets) + + def show_rauser(_t): + blit_scaled_centered(screen, rauser, WINDOW_W, WINDOW_H) + quit_requested = wait_or_skip(clock, 3.0, show_rauser) + if quit_requested: + return True + + # 2) schwarze Pause + screen.fill(BG_COLOR) + pygame.display.flip() + quit_requested = wait_or_skip(clock, 0.5, lambda t: screen.fill(BG_COLOR)) + if quit_requested: + return True + + # 3) Factor5 (statisch) + factor5 = build_letterboxed(assets.pcc_surface('FACTOR5')) + + def show_factor5(_t): + blit_scaled_centered(screen, factor5, WINDOW_W, WINDOW_H) + quit_requested = wait_or_skip(clock, 3.0, show_factor5) + if quit_requested: + return True + + # 4) schwarze Pause + quit_requested = wait_or_skip(clock, 0.5, lambda t: screen.fill(BG_COLOR)) + if quit_requested: + return True + + # 5) Kellogg's-Logo + "praesentiert"-Palettenfade + kw, kh, kidx, kpal = assets.pcc('KELLOGGS') + + def show_kelloggs(t): + frame = build_kelloggs_frame(kw, kh, kidx, kpal, t) + letterboxed = build_letterboxed(frame) + blit_scaled_centered(screen, letterboxed, WINDOW_W, WINDOW_H) + quit_requested = wait_or_skip(clock, PRAESENTIERT_FADE_SECONDS, show_kelloggs) + if quit_requested: + return True + # kurz mit vollem Text stehen lassen + quit_requested = wait_or_skip(clock, 1.0, lambda t: show_kelloggs(1.0)) + if quit_requested: + return True + + # 6) schwarze Pause + quit_requested = wait_or_skip(clock, 0.5, lambda t: screen.fill(BG_COLOR)) + if quit_requested: + return True + + # 7) Titelbild -- Vorhang-Wipe (obere Haelfte faehrt runter, untere hoch) + title_img, half_h = build_title_image(assets) + full_w, full_h = title_img.get_size() + + def show_title_wipe(t): + canvas = pygame.Surface((full_w, full_h)) + canvas.fill((0, 0, 0)) + reveal = int(half_h * t) + if reveal > 0: + top_src = pygame.Surface.subsurface(title_img, (0, half_h - reveal, full_w, reveal)) + canvas.blit(top_src, (0, 0)) + bottom_src = pygame.Surface.subsurface(title_img, (0, half_h, full_w, reveal)) + canvas.blit(bottom_src, (0, full_h - reveal)) + blit_scaled_centered(screen, canvas, WINDOW_W, WINDOW_H) + quit_requested = wait_or_skip(clock, 3.0, show_title_wipe) + if quit_requested: + return True + + # danach: fertig zusammengesetztes Titelbild halten + def show_title_full(_t): + blit_scaled_centered(screen, title_img, WINDOW_W, WINDOW_H) + quit_requested = wait_or_skip(clock, 3.0, show_title_full) + return quit_requested + + +def main(): + once = '--once' in sys.argv + pygame.init() + screen = pygame.display.set_mode((WINDOW_W, WINDOW_H)) + pygame.display.set_caption("Tony & Friends in Kellogg's Land -- Intro (Remake)") + clock = pygame.time.Clock() + + assets = Assets(DAT_PATH) + + quit_requested = run_once(screen, clock, assets) + while not quit_requested and not once: + quit_requested = run_once(screen, clock, assets) + + pygame.quit() + + +if __name__ == '__main__': + main() diff --git a/tools/kellogg_formats.py b/tools/kellogg_formats.py index f6842b1..74851fb 100644 --- a/tools/kellogg_formats.py +++ b/tools/kellogg_formats.py @@ -61,6 +61,47 @@ def get_pcx_palette(pcc_bytes): return [tuple(pal_bytes[i:i+3]) for i in range(0, 768, 3)] +def load_pcc(pcc_bytes): + '''Decode a .PCC (= plain PCX v5, 8bpp, RLE, own trailing 256-color + palette) via Pillow's battle-tested PCX decoder instead of a hand-rolled + RLE parser -- see git history 2026-07-22 (commits 5efba4b/62af15e/3410a53) + for why: TWO from-scratch RLE implementations silently produced visibly + wrong pixels (shifted/duplicated content) for full-screen 320x200 images + (KARTE.PCC, KELLOGGS.PCC) despite being "verified" -- Pillow avoids that + whole class of bug entirely. + + Returns (width, height, indices, palette): + indices -- flat list[int] of palette indices, len width*height, row-major + palette -- list of 256 (r,g,b) tuples (this file's OWN embedded palette, + not any global one) + ''' + from PIL import Image + import io + img = Image.open(io.BytesIO(pcc_bytes)) + img.load() + if img.mode != 'P': + raise ValueError(f'expected palette (P) mode PCX, got {img.mode}') + width, height = img.size + indices = list(img.getdata()) + raw_pal = img.getpalette() # flat [r,g,b, r,g,b, ...], may be shorter than 768 + raw_pal = (raw_pal + [0] * 768)[:768] + palette = [tuple(raw_pal[i:i+3]) for i in range(0, 768, 3)] + return width, height, indices, palette + + +def pcc_to_rgba(pcc_bytes, transparent_index=None): + '''Convenience wrapper around load_pcc(): returns (width, height, rgba_bytes) + ready for e.g. pygame.image.frombuffer(rgba, (w,h), 'RGBA') or PIL Image.frombytes. + If transparent_index is given, that palette index becomes alpha=0.''' + width, height, indices, palette = load_pcc(pcc_bytes) + out = bytearray(width * height * 4) + for i, idx in enumerate(indices): + r, g, b = palette[idx & 0xFF] + a = 0 if (transparent_index is not None and idx == transparent_index) else 255 + out[i*4:i*4+4] = bytes([r, g, b, a]) + return width, height, bytes(out) + + def get_bob_palette(entries, name): '''name = basename without extension, e.g. 'TONY' for TONY.BOB. Implements the palette-selection rules from Form1.cs (BOB case).''' diff --git a/tools/pcc_to_png.py b/tools/pcc_to_png.py index 29f66e1..0d05575 100644 --- a/tools/pcc_to_png.py +++ b/tools/pcc_to_png.py @@ -1,105 +1,42 @@ #!/usr/bin/env python3 -"""Finaler PCC-Decoder fuer Kellogg's Tony and Friends (2026-07-22, ARIA). +"""PCC-Decoder fuer Kellogg's Tony and Friends -- nutzt Pillows PCX-Decoder. -Format (verifiziert gegen MENU.PCC/RAUSER1-3/FACTOR5, visuell + byte-exakt): -- Byte 0-15: echter PCX-Header-Anfang (Manufacturer=0x0A, Version=5, - Encoding=1/RLE, BPP=8, dann Xmin/Ymin/Xmax/Ymax als LE16 bei Offset 4-11). - WICHTIG (Bugfix 2026-07-22): Width/Height MUESSEN aus Xmax-Xmin+1 / - Ymax-Ymin+1 berechnet werden. Die 2 LE16-Werte bei Offset 12-15 sehen fuer - Vollbild-Screens (320x200) zufaellig identisch aus und wurden erst dafuer - gehalten -- sind aber tatsaechlich NICHT die Bilddimensionen (vermutlich - ein DPI/Reserved-Feld wie im echten 128-Byte-PCX-Header), sondern ein - Konstantwert der bei kleinen Sprites/Logos (z.B. RAUSER1.PCC: 182x46, - FAC0.PCC: 16x16) komplett falsch war und zu kaputten/leeren Bildern fuehrte. - Mit dem Xmax/Ymax-Fix konsumiert z.B. FACTOR5.PCC exakt 100% der - komprimierten Bytes (51609/51609) statt vorher mit falscher Hoehe (200 - statt echten 199) zu ueberlaufen. -- Byte 16 .. (len-769): RLE-komprimierte Pixel-Indexdaten, SCANLINE-weise - decodiert (pro Zeile wird bis width Pixel gefuellt, ueberschuessige Pixel - eines Runs am Zeilenende werden verworfen -- klassisches PCX-Verhalten). - RLE-Tupel: Byte mit oberen 2 Bits gesetzt (0xC0-0xFF) = Lauflaenge (&0x3F), - gefolgt von einem Wert-Byte. Sonst literaler Pixel. -- Letzte 769 Bytes: 0x0C-Marker + 768 Byte (256 x RGB) eingebettete Palette - (klassische PCX-v5-256-Farben-Erweiterung). PRO DATEI eigene Palette, - keine globale Palette noetig. +.PCC ist stinknormales PCX v5 (8bpp, RLE, eigene 256-Farb-Palette am +Dateiende) -- bestaetigt gegen https://github.com/movAX13h/tony-and-friends-in-kelloggs-land. -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%). +REGRESSIONS-WARNUNG (gefunden 2026-07-22 abends): Commit 62af15e hatte hier +schon mal korrekt auf ImageMagick (`convert pcx:...`) umgestellt, nachdem +ZWEI eigene Hand-RLE-Parser nachweislich falsche Pixel lieferten (Bild +verschoben/dupliziert bei Vollbild-Screens wie KARTE.PCC/KELLOGGS.PCC). +Der naechste Commit (3410a53, eigentlich nur fuer BOB/ICO/MAP gedacht) hat +diese Datei aber versehentlich durch eine AELTERE Version mit dem kaputten +Hand-Decoder ersetzt -- die bereits committeten PNGs in png_out/ waren davon +zum Glueck nicht betroffen (die sind noch von der guten ImageMagick-Version), +aber ein Rerun dieses Skripts haette wieder kaputte Bilder erzeugt. Fix jetzt: +Pillow statt Hand-RLE oder externem `convert`-Subprocess -- kein Format- +Parsing mehr in eigenem Code, nutzt tools/kellogg_formats.load_pcc(). Nutzung: python3 pcc_to_png.py -Schreibt ein PPM und konvertiert via ImageMagick `convert` zu PNG. """ -import sys, struct, subprocess, os +import sys, os +sys.path.insert(0, os.path.dirname(__file__)) +from kellogg_formats import load_pcc -def decode_pcc(data): - manuf, version, encoding, bpp = data[0], data[1], data[2], data[3] - xmin, ymin, xmax, ymax = struct.unpack('= n: - break - val = pixel_region[i]; i += 1 - row_out.extend([val] * count) - else: - row_out.append(b) - row_out = row_out[:width] - if len(row_out) < width: - row_out.extend([0] * (width - len(row_out))) - out.extend(row_out) - - return dict(width=width, height=height, marker_ok=(marker == 0x0C), - pixels=bytes(out), palette=palette, - consumed=i, pixel_region_len=n) - -def write_png(pixels, palette, width, height, out_png): - ppm = out_png + '.ppm' - with open(ppm, 'wb') as f: - f.write(f'P6\n{width} {height}\n255\n'.encode()) - buf = bytearray() - for px in pixels[:width*height]: - r, g, b = palette[px] - buf.extend([r, g, b]) - f.write(bytes(buf)) - subprocess.run(['convert', ppm, out_png], check=True) - os.remove(ppm) def main(): inp, outp = sys.argv[1], sys.argv[2] with open(inp, 'rb') as f: data = f.read() - res = decode_pcc(data) - print(f"{inp}: {res['width']}x{res['height']} marker_ok={res['marker_ok']} " - f"consumed={res['consumed']}/{res['pixel_region_len']}") - write_png(res['pixels'], res['palette'], res['width'], res['height'], outp) + width, height, indices, palette = load_pcc(data) + print(f"{inp}: {width}x{height}") + + from PIL import Image + img = Image.new('P', (width, height)) + img.putpalette([c for rgb in palette for c in rgb]) + img.putdata(indices) + img.convert('RGB').save(outp) print(f"-> {outp}") + if __name__ == '__main__': main()