Boot-Sequenz vermessen + Intro-Player gebaut, echten PCC-Decoder-Regressionsbug gefixt
- Regression gefunden+gefixt: pcc_to_png.py war durch einen frueheren Commit versehentlich auf den kaputten Hand-RLE-Decoder zurueckgefallen (der ImageMagick-Fix ging verloren). Jetzt: kellogg_formats.load_pcc() nutzt Pillows PCX-Decoder direkt, kein eigener RLE-Code mehr im Projekt. Alle 77 PCC neu gerendert, AE=0 (pixelidentisch) zu den alten guten PNGs. - Boot-Sequenz per echter DOSBox-Screenshot-Serie (0.3s-Intervall) vermessen: Rauser-Karte -> Factor5-Logo (statisch, keine Animation) -> Kellogg's-Logo mit Palette-Fade des "praesentiert"-Texts (Indizes 34/42/43/44/45) -> Titelbild "Tony & Friends in Kellogg's Land" mit echtem Vorhang-Wipe-Effekt (320x480 aus 4 Quadranten KELL256A-D, obere Haelfte faehrt runter, untere hoch, treffen sich in der Mitte). Korrigiert eine fruehere Fehlannahme: der Wipe sitzt auf dem TITELBILD, nicht auf dem Kellogg's-Markenlogo. - tools/intro_sequence.py: erster spielbarer pygame-Meilenstein, baut die komplette Sequenz nach (Asset-Loading, Palette-Rendering, Timing, echter Palette-Fade, Wipe-Effekt). Headless getestet, visuell verifiziert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -778,3 +778,114 @@ zu einer Sequenz zusammengesetzt):**
|
|||||||
geht und einfach hier weitergelesen werden kann.
|
geht und einfach hier weitergelesen werden kann.
|
||||||
|
|
||||||
Laeuft jetzt selbstaendig weiter, keine Rueckfrage noetig.
|
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.
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -61,6 +61,47 @@ def get_pcx_palette(pcc_bytes):
|
|||||||
return [tuple(pal_bytes[i:i+3]) for i in range(0, 768, 3)]
|
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):
|
def get_bob_palette(entries, name):
|
||||||
'''name = basename without extension, e.g. 'TONY' for TONY.BOB.
|
'''name = basename without extension, e.g. 'TONY' for TONY.BOB.
|
||||||
Implements the palette-selection rules from Form1.cs (BOB case).'''
|
Implements the palette-selection rules from Form1.cs (BOB case).'''
|
||||||
|
|||||||
+26
-89
@@ -1,105 +1,42 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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):
|
.PCC ist stinknormales PCX v5 (8bpp, RLE, eigene 256-Farb-Palette am
|
||||||
- Byte 0-15: echter PCX-Header-Anfang (Manufacturer=0x0A, Version=5,
|
Dateiende) -- bestaetigt gegen https://github.com/movAX13h/tony-and-friends-in-kelloggs-land.
|
||||||
Encoding=1/RLE, BPP=8, dann Xmin/Ymin/Xmax/Ymax als LE16 bei Offset 4-11).
|
|
||||||
WICHTIG (Bugfix 2026-07-22): Width/Height MUESSEN aus Xmax-Xmin+1 /
|
|
||||||
Ymax-Ymin+1 berechnet werden. Die 2 LE16-Werte bei Offset 12-15 sehen fuer
|
|
||||||
Vollbild-Screens (320x200) zufaellig identisch aus und wurden erst dafuer
|
|
||||||
gehalten -- sind aber tatsaechlich NICHT die Bilddimensionen (vermutlich
|
|
||||||
ein DPI/Reserved-Feld wie im echten 128-Byte-PCX-Header), sondern ein
|
|
||||||
Konstantwert der bei kleinen Sprites/Logos (z.B. RAUSER1.PCC: 182x46,
|
|
||||||
FAC0.PCC: 16x16) komplett falsch war und zu kaputten/leeren Bildern fuehrte.
|
|
||||||
Mit dem Xmax/Ymax-Fix konsumiert z.B. FACTOR5.PCC exakt 100% der
|
|
||||||
komprimierten Bytes (51609/51609) statt vorher mit falscher Hoehe (200
|
|
||||||
statt echten 199) zu ueberlaufen.
|
|
||||||
- Byte 16 .. (len-769): RLE-komprimierte Pixel-Indexdaten, SCANLINE-weise
|
|
||||||
decodiert (pro Zeile wird bis width Pixel gefuellt, ueberschuessige Pixel
|
|
||||||
eines Runs am Zeilenende werden verworfen -- klassisches PCX-Verhalten).
|
|
||||||
RLE-Tupel: Byte mit oberen 2 Bits gesetzt (0xC0-0xFF) = Lauflaenge (&0x3F),
|
|
||||||
gefolgt von einem Wert-Byte. Sonst literaler Pixel.
|
|
||||||
- Letzte 769 Bytes: 0x0C-Marker + 768 Byte (256 x RGB) eingebettete Palette
|
|
||||||
(klassische PCX-v5-256-Farben-Erweiterung). PRO DATEI eigene Palette,
|
|
||||||
keine globale Palette noetig.
|
|
||||||
|
|
||||||
BEKANNTER OFFENER BUG (Stand 2026-07-22, noch nicht geloest):
|
REGRESSIONS-WARNUNG (gefunden 2026-07-22 abends): Commit 62af15e hatte hier
|
||||||
KELLOGGS.PCC und KARTE.PCC (volle 320x200-Screens) zeigen weiterhin einen
|
schon mal korrekt auf ImageMagick (`convert pcx:...`) umgestellt, nachdem
|
||||||
lokalen Deko-Fehler (ein "Kerbe"/Notch-Artefakt rechts neben dem Kellogg's-
|
ZWEI eigene Hand-RLE-Parser nachweislich falsche Pixel lieferten (Bild
|
||||||
Schriftzug, roter/gelber Fleck der nicht ins Referenz-Screenshot passt).
|
verschoben/dupliziert bei Vollbild-Screens wie KARTE.PCC/KELLOGGS.PCC).
|
||||||
Ausgeschlossen als Ursache: (a) horizontales Rollen/Verschieben des ganzen
|
Der naechste Commit (3410a53, eigentlich nur fuer BOB/ICO/MAP gedacht) hat
|
||||||
Bilds -- getestet mit 18 Shift-Kandidaten, Artefakt bleibt IMMER an
|
diese Datei aber versehentlich durch eine AELTERE Version mit dem kaputten
|
||||||
derselben Position relativ zum Bildinhalt, nicht zur Leinwand -> kein
|
Hand-Decoder ersetzt -- die bereits committeten PNGs in png_out/ waren davon
|
||||||
Rotations-/Scroll-Bug. (b) Zeilenweises Verwerfen von RLE-Overshoot --
|
zum Glueck nicht betroffen (die sind noch von der guten ImageMagick-Version),
|
||||||
zeilenbasierte und "kontinuierliche" (ohne Row-Reset) Decodierung liefern
|
aber ein Rerun dieses Skripts haette wieder kaputte Bilder erzeugt. Fix jetzt:
|
||||||
BYTE-IDENTISCHE Ergebnisse fuer diese Dateien. Vermutung: entweder eine
|
Pillow statt Hand-RLE oder externem `convert`-Subprocess -- kein Format-
|
||||||
Palette-Fehlzuordnung fuer einzelne Indizes, oder eine RLE-Ambiguitaet bei
|
Parsing mehr in eigenem Code, nutzt tools/kellogg_formats.load_pcc().
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
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('<HHHH', data[4:12])
|
|
||||||
width, height = xmax - xmin + 1, ymax - ymin + 1
|
|
||||||
pal_start = len(data) - 769
|
|
||||||
marker = data[pal_start]
|
|
||||||
pixel_region = data[16:pal_start]
|
|
||||||
pal_bytes = data[pal_start+1:pal_start+1+768]
|
|
||||||
palette = [(pal_bytes[i], pal_bytes[i+1], pal_bytes[i+2]) for i in range(0, 768, 3)]
|
|
||||||
|
|
||||||
out = bytearray()
|
|
||||||
i = 0
|
|
||||||
n = len(pixel_region)
|
|
||||||
for row in range(height):
|
|
||||||
row_out = bytearray()
|
|
||||||
while len(row_out) < width and i < n:
|
|
||||||
b = pixel_region[i]; i += 1
|
|
||||||
if (b & 0xC0) == 0xC0:
|
|
||||||
count = b & 0x3F
|
|
||||||
if i >= n:
|
|
||||||
break
|
|
||||||
val = pixel_region[i]; i += 1
|
|
||||||
row_out.extend([val] * count)
|
|
||||||
else:
|
|
||||||
row_out.append(b)
|
|
||||||
row_out = row_out[:width]
|
|
||||||
if len(row_out) < width:
|
|
||||||
row_out.extend([0] * (width - len(row_out)))
|
|
||||||
out.extend(row_out)
|
|
||||||
|
|
||||||
return dict(width=width, height=height, marker_ok=(marker == 0x0C),
|
|
||||||
pixels=bytes(out), palette=palette,
|
|
||||||
consumed=i, pixel_region_len=n)
|
|
||||||
|
|
||||||
def write_png(pixels, palette, width, height, out_png):
|
|
||||||
ppm = out_png + '.ppm'
|
|
||||||
with open(ppm, 'wb') as f:
|
|
||||||
f.write(f'P6\n{width} {height}\n255\n'.encode())
|
|
||||||
buf = bytearray()
|
|
||||||
for px in pixels[:width*height]:
|
|
||||||
r, g, b = palette[px]
|
|
||||||
buf.extend([r, g, b])
|
|
||||||
f.write(bytes(buf))
|
|
||||||
subprocess.run(['convert', ppm, out_png], check=True)
|
|
||||||
os.remove(ppm)
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
inp, outp = sys.argv[1], sys.argv[2]
|
inp, outp = sys.argv[1], sys.argv[2]
|
||||||
with open(inp, 'rb') as f:
|
with open(inp, 'rb') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
res = decode_pcc(data)
|
width, height, indices, palette = load_pcc(data)
|
||||||
print(f"{inp}: {res['width']}x{res['height']} marker_ok={res['marker_ok']} "
|
print(f"{inp}: {width}x{height}")
|
||||||
f"consumed={res['consumed']}/{res['pixel_region_len']}")
|
|
||||||
write_png(res['pixels'], res['palette'], res['width'], res['height'], outp)
|
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}")
|
print(f"-> {outp}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user