3-D Ultra spannend: Digitale Archäologie in „3-D Ultra Pinball: Creep Night“

English version here! / Extractor Tool / Zur kompletten Infosite

Manchmal beginnt eine technische Untersuchung völlig ungeplant – und enden damit, dass man nicht nur eine Engine auseinanderbaut, sondern dabei auch unbekannte Eastereggs und Cheats entdeckt.

In meinem Fall wollte ich eigentlich nur 3-D Ultra Pinball: Creep Night von Sierra aus dem Jahr 1996 auf einem emulierten Windows-98-Rechner wieder spielen. Das Spiel ist für mich seit meiner Kindheit so eines der Games, deren Abläufe man Jahrzehnte später erstaunlicherweise immer noch im „Muscle Memory“ hat.

Beim Spielen fiel mir diesmal allerdings etwas auf, über das ich als Kind selbstverständlich nie nachgedacht hatte: Trotz seines Namens ist 3-D Ultra Pinball kein 3D-Spiel im heutigen Sinne.

Die Tische sehen dreidimensional aus. Figuren, Kugeln, Rampen und Animationen ebenfalls. Aber es gibt keine in Echtzeit gerenderte polygonale 3D-Welt. Stattdessen arbeitet das von Dynamix entwickelte Spiel mit vorgerenderten 2D-Grafiken.

Das war der Beginn einer kleinen Reise in die Innereien der Engine. 😊

„3-D“ ohne 3D

Das Grundprinzip ist aus heutiger Sicht ebenso einfach wie clever: Die Spieltische wurden offenbar mit 3D-Werkzeugen gestaltet und anschließend als fertige Bilder gerendert. Im Spiel selbst liegt für jeden Tisch ein vollständiger Hintergrund mit einer Auflösung von 640 × 480 Pixeln vor.

Darüber zeichnet die Engine kleinere Sprites für bewegliche Elemente wie Kugeln, Figuren, Bumper und Animationen.

Vereinfacht sieht das Rendering damit ungefähr so aus:

Vorgerenderter 640×480-Hintergrund
                +
        animierte Sprites
                +
             Kugel
                +
      Flipper / Targets / Effekte
                =
         fertiges Spielbild

Das „3-D“ beschreibt also vor allem die optische Gestaltung der Assets. Zur Laufzeit arbeitet das Spiel weitgehend mit 2D-Grafiken.

Für einen Windows-PC von 1996 war das eine ausgesprochen sinnvolle Lösung: Die aufwendige 3D-Arbeit wurde vorab erledigt, während der Rechner des Spielers nur noch vorbereitete Pixelgrafiken zeichnen musste.

Ein Blick in RESOURCE.001

Auf der CD befindet sich im Spielverzeichnis unter anderem das Dateipaar:

RESOURCE.MAP
RESOURCE.001

RESOURCE.MAP dient dabei als Inhaltsverzeichnis. RESOURCE.001 enthält die eigentlichen Daten.

In meiner Version des Spiels befinden sich darin insgesamt 854 einzelne Ressourcen:

430 × WAV
275 × BMP
109 × ANM
 19 × ITF
  7 × PAL
  4 × TBL
  4 × TBM
  4 × RES
  2 × PFT

Die Dateiendungen sind dabei etwas irreführend. Eine Ressource namens GHOST0.BMP ist beispielsweise keine normale Windows-BMP-Datei. Der Inhalt beginnt stattdessen mit einem Dynamix-eigenen Header BMP:. Ähnliches findet man bei Animationen (ANM:) und weiteren Datenformaten.

Die Ressourcen selbst sind in RESOURCE.001 hintereinander abgelegt. RESOURCE.MAP enthält zu jedem Eintrag einen Hash und einen Offset. An diesem Offset findet man wiederum einen DOS-kompatiblen Dateinamen, die Größe und anschließend die eigentlichen Daten.

Was macht die „Large“-Installation?

Auch eine kleine Frage aus dem damaligen Setup ließ sich damit beantworten.

Creep Night bietet bei der Installation eine „Large“-Variante an. Dabei wird die Datei ENGLISH\LARGE\OVERRIDE.DAT auf die Festplatte kopiert. Der Installer erklärt, dass anschließend praktisch nur noch Audio von CD gelesen werden muss.

OVERRIDE.DAT enthält Index und Ressourcendaten gemeinsam und ist damit im Wesentlichen eine lokale Variante des RESOURCE.MAP/RESOURCE.001-Systems.

Während RESOURCE.001 854 Ressourcen enthält, befinden sich in OVERRIDE.DAT 817 davon. Die fehlenden 37 Dateien sind:

SX_4000.WAV
...
SX_4036.WAV

und das sind tatsächlich ausschließlich lange Audiodateien. Zusammen ergeben sie rund 34 Minuten Audio und etwa 90 MB Daten.

Damit lässt sich gut zeigen, was „Large Installation“ 1996 bedeutete: Fast sämtliche Grafiken, Animationen und kurzen Sounds wandern auf die schnelle Festplatte – nur die großen Audio-Tracks bleiben aus Platzgründen auf der CD.

GHOST0.BMP: Vier Geister in zwei Kilobyte

Besonders interessant wurde die Datei GHOST0.BMP. Sie ist gerade einmal ca. 2 KB groß, enthält aber nicht ein, sondern vier verschiedene Sprite-Frames:

Frame 1: 56 × 38 Pixel
Frame 2: 56 × 64 Pixel
Frame 3: 64 × 62 Pixel
Frame 4: 64 × 51 Pixel

Jeder Frame besitzt einen kleinen Header mit Breite, Höhe, einem Encoding-Byte sowie komprimierter und unkomprimierter Größe.

Beim ersten Frame sieht das beispielsweise so aus:

38 00          Breite: 56
26 00          Höhe:   38
83             Encoding: 0x83
61 01 00 00    komprimierter Block: 353 Bytes
50 08 00 00    unkomprimiert: 2128 Bytes

Und hier findet sich bereits ein entscheidender Hinweis:

56 × 38 = 2128

Nach der Dekompression existiert also genau ein Byte pro Pixel. Die Grafiken sind klassische palettenbasierte 8-Bit-Bilder.

Was bedeutet 0x83?

Genau diese Kennung war zunächst das große Rätsel. Für das Encoding gibt es keinerlei Dokumentation, nur ein paar Hinweise im Internet von anderen Menschen, die bereits versucht haben, die Dynamix-eigene Kompression zu reverse engineeren. Aber durch Analyse der originalen PINBALL2.EXE ließ sich schließlich der entsprechende Codepfad finden.

Der Originalcode liest das Encoding-Byte und führt zunächst sinngemäß Folgendes aus:

encoding = encoding & 0x7F

Aus 0x83 wird also 0x03. Anschließend erfolgt ein Sprung zum Decoder für Kompressionstyp 3. Dieser ließ sich aus dem x86-Code rekonstruieren.

Der Dynamix-Type-3-Codec

Der Algorithmus ist eine kompakte Form von LZ-Kompression. Der Datenstrom enthält zunächst die Anzahl der folgenden Tokens. Danach kommt ein Block aus Flag-Bits und anschließend die eigentlichen Tokendaten.

Für jedes Token gibt es zwei Möglichkeiten:

Flag = 0
→ nächstes Byte ist ein echter Pixelwert

Flag = 1
→ kopiere bereits dekomprimierte Pixel erneut

Eine Rückreferenz benötigt zwei Bytes beziehungsweise 16 Bit.

Diese teilen sich sinngemäß folgendermaßen auf:

DDDDDDDD DDLLLLLL

Die unteren sechs Bits kodieren die Länge:

length = value + 3

Die oberen zehn Bits kodieren den Abstand:

distance = value + 1

Damit kann die Datei beispielsweise ausdrücken:

„Nimm die Pixel von vor 56 Bytes
und kopiere davon noch einmal 42 Stück.“

Gerade bei Sprites mit großen gleichförmigen oder leeren Bereichen spart das enorm viel Platz.

Der erste Ghost-Frame benötigt unkomprimiert 2.128 Bytes, sein komprimierter Datenstrom dagegen nur ungefähr 350 Bytes.

Mit dieser Kompression sind alle Animationen, Overlays, Buttons und so weiter in der Datei RESOURCE.001 gespeichert, eine nach dem anderen als fortlaufender Stream. Korrekt extrahiert kommt man so an alle Sprites und Grafiken des Spiels – und könnte so quasi ein Daumenkino aus diesen machen.

Und wie funktioniert die Transparenz?

Hier wird es aus heutiger Sicht besonders charmant, weil man mit den Limitationen der damaligen Technik arbeiten musste. Moderne Grafik kennt Alpha-Kanäle. Ein Pixel kann beispielsweise zu 25, 50 oder 80 Prozent transparent sein und wird entsprechend mit dem Hintergrund verrechnet.

Davon konnten die Entwickler von Creep Night nur träumen.

Nach der Dekompression besteht ein Sprite ausschließlich aus 8-Bit-Palettenwerten. Beim ersten Frame von GHOST0.BMP gibt es beispielsweise 2.128 Pixel. Davon besitzen 1.425 Pixel den Wert 0.

Das Bild enthält damit sehr große Bereiche, die quasi so aussehen:

0 0 0 0 0 0 0
0 0 0 X X 0 0
0 0 X X X X 0
0 X X X X X X
0 0 X X X X 0
0 0 0 X X 0 0

Alles deutet darauf hin, dass Palette Index 0 als Color Key behandelt wird:

Pixel = 0
→ vorhandenen Hintergrund nicht überschreiben

Pixel != 0
→ Sprite-Pixel zeichnen

Das ist keine Alpha-Transparenz. Ein Pixel ist entweder vollständig sichtbar oder überhaupt nicht vorhanden.

Die Grafikdaten selbst sprechen sehr deutlich für dieses Verfahren. Die genaue Blit-Routine der Engine müsste für einen endgültigen Beweis allerdings noch vollständig auf Instruktionsebene nachvollzogen werden. (Freiwillige vor!)

GHOST0, GHOST1 und GHOST2

Direkt hintereinander befinden sich drei interessante Ressourcen:

GHOST0.BMP
GHOST1.BMP
GHOST2.BMP

Sie enthalten:

GHOST0:  4 Frames
GHOST1:  9 Frames
GHOST2: 12 Frames

Insgesamt liegen dort also 25 einzelne Bilder verschiedener Geisteranimationen.

Beim Export sieht man sehr schön, was hinter dem Namen „3-D Ultra“ steckt: Die Figuren haben eindeutig den Look vorgerenderter 3D-Modelle – im Spiel selbst sind sie jedoch kleine 2D-Sprites mit palettierten Farben und Color-Key-Transparenz.

Das ist kein Makel, sondern ein schönes Beispiel dafür, wie geschickt Entwickler Mitte der 1990er-Jahre mit den verfügbaren Ressourcen gearbeitet haben.

Übrigens handelt es sich bei dem Typ um den Geist, der um die Türme des „The Castle“-Tischs schwebt. 😉

Der kommt auch an anderer Stelle vor, daher die unbenutzten Animationsframes oben im Sheet.

Der merkwürdige „Wobbel“ – wie erzeugt die Engine räumliche Tiefe?

Genau diese Frage war ursprünglich der Auslöser für die ganze Untersuchung: An einigen Stellen fällt auf modernen Displays ein merkwürdiger grafischer Effekt auf. Wenn die Kugel hinter einem Objekt verschwindet, wirkt dessen Rand plötzlich leicht verändert – fast so, als würde ein Stück des Hintergrundbildes nochmals darübergelegt, dabei aber nicht hundertprozentig deckungsgleich positioniert.

Ein Vergleich zweier unmittelbar aufeinanderfolgender Screenshots macht den Effekt deutlich:

Screenshot 1: Die Kugel befindet sich nicht hinter dem Vordergrundobjekt. Passt so.

Screenshot 2: Die Kugel befindet sich dahinter. Gleichzeitig erscheint an dem darüberliegenden Objekt ein sichtbarer Saum beziehungsweise eine leichte Veränderung der Pixel. Bei den Screenshots am besten sichtbar, wenn man zwischen den beiden hin und her wechselt, oder wenn man es im Spiel in Bewegung sieht.

Interessant ist dabei, dass sich zwischen beiden Bildern nicht nur die kleine Fläche der Kugel verändert. Die abweichenden Pixel erstrecken sich über einen deutlich größeren, annähernd rechteckigen Bereich des Vordergrundobjekts.

Das legt eine weitere Technik nahe, die bei einem rein zweidimensionalen Spiel sehr sinnvoll wäre: Occlusion Sprites beziehungsweise Vordergrund-Layer.

Die Engine könnte zunächst Tisch und Kugel zeichnen und anschließend – sobald sich die Kugel „hinter“ einem Bauteil befindet – einen vorbereiteten Ausschnitt dieses Bauteils nochmals darüberzeichnen:

Tischhintergrund
      ↓
Kugel zeichnen
      ↓
Liegt Kugel hinter einem Objekt?
      ↓
Vordergrund-/Occlusion-Sprite
noch einmal darüberzeichnen

Damit lässt sich eine Kugel problemlos hinter Rampen, Rohren oder anderen Tischteilen verschwinden lassen, ohne dass hierfür echte 3D-Geometrie oder ein Z-Buffer benötigt wird.

Der sichtbare „Wobbel“ könnte dann durch die Vordergrundgrafik selbst entstehen. Bereits eine minimal andere Dithering-Struktur, eine abweichende Palette oder eine Positionierung um nur einen Pixel würde auf einem modernen LCD als feiner Rand auffallen.

Das gleiche gilt auch, wenn wir uns mal das Geister-GIF von oben genauer anschauen: Achtet mal auf die Steinstruktur des Turms. Sobald der Sprite des Geistes angezeigt wird, verschieben sich die Hintergrund-Pixel ein wenig. Es scheint also, dass der Palette Index 0 (= zeichne den Hintergrund hier nochmal) nicht ganz perfekt funktioniert:

Der Effekt ist am deutlichsten, wenn das GIF endet und sich wiederholt – dann wird der Hintergrund quasi „zurückgesetzt“.

Auf einem CRT-Monitor der 1990er-Jahre wurden solche Unterschiede durch das analoge Signal, die Loch- beziehungsweise Streifenmaske und die allgemeine Charakteristik der Bildröhre wesentlich stärker optisch vermischt.

Ein modernes Display zeigt dagegen jeden einzelnen emulierten 640×480-Pixel gnadenlos exakt.

Fundgrube Resourcen-Datei

Ich liebe es ja, durch solche Dateien zu stöbern, von denen die Entwickler dachten, niemand Externes wird sie je sehen. Da findet man manchmal echt spannende Überbleibsel oder Eastereggs. (Grüße an The Cutting Room Floor!)

Ein paar spannende Dinge sind mir noch aufgefallen:

Das mit den Kühen muss ich mir irgendwann genauer anschauen. Kühe sind eine merkwürdige, aber beliebte Tradition für Eastereggs bei echten Flipperautomaten, das wäre also eine schöne Anlehnung daran. Ich würde sowieso sagen, dass die Entwickler durchaus die „richtigen“ Flipperautomaten kannten, denn im Hilfe-Menü gibt es ein extra Kapitel für Buchtipps rund um das Flippern, mit Büchern von z.B. Roger Sharpe – eine schöne Referenz an das Hobby!

Nun… schauen wir uns das doch mal an! 😊

Neue Cheats und Easter Eggs

Soweit ich weiß, sind die folgenden Infos bisher undokumentiert und neu. Ein bisschen stolz bin ich ja jetzt.

Wenn man die Highscore-Tabelle öffnet, einfach „data“ eingeben. Einfach ins Leere tippen und man hört eine Stimme „Excellent!“ sagen. Das bedeutet, dass der Cheat- bzw. Debug-Modus ist nun aktiviert (die Variable DYNAMIX ist auf 1 gesetzt worden).

Und jetzt wird es spannend:

Wenn man nun im Spiel „cows“ eingibt, hört man eine Kuh muhen. Das bestätigt, dass der Flag cow_mode nun gesetzt ist. Wenn man das Shooter-Bonusspiel nun startet, werden alle Sprites durch Kühe ersetzt – manche laufen, manche rennen und eine Kuh fährt auf einem Quad durch’s Bild. Um dieses Bonusspiel zu starten, müssen beim Tisch „The Castle“ alle Modi gespielt worden sein und die Kugel dann erfolgreich ins Portal geschossen werden.

Ein weiterer hilfreicher Entwicklercode ist an der Stelle „fin“. Wenn man diesen während eines Spiels eingibt, werden alle Modi automatisch als erledigt markiert und man kann zum finalen Boss des Tisches übergehen. Das scheint eine Abkürzung für Tests der Entwickler zu sein (vermute ich mal?). Der Code funktioniert auch nur, wenn als Spielmodus „Alle Tische“ gewählt wird. Wenn man das Spiel nur mit explizit einem Tisch startet, ist er wirkungslos.

Es gibt außerdem noch einen Status mcow mit der dazu passenden Animation:

Ich konnte bislang aber nicht herausfinden, wie man dieses Easteregg triggert. Klar ist nur anhand des Codes, dass die Kühe in allen drei Tischen die Goblins im jeweiligen Modus ersetzen würden: Anstatt die Golbins über sieben verschiedene Positionen zu verfolgen und mittels Flipperkugel platt zu machen, würde die Kuh anscheinend auf der Position im Kreis laufen. Es scheint aber, als wäre der dazugehörige Trigger nie eingebaut worden. Schade.

Also habe ich einfach ein kleines Patch-Tool geschrieben, welche die Flag cow_mode einfach auf auf dieses Easter Egg anwendet. Einfach herunterladen, PINBALL2.EXE patchen und ausprobieren (bei den Highscores „DATA“ eingeben und im Spiel „COWS“ eingeben). Mehr Infos in der README.MD.

Und so sieht das dann aus – ziemlich cool, finde ich! 😊

Es gibt übrigens noch ein undokumentiertes Extra: Die Boss-Taste „U“. Wenn auf der Tastatur im Spiel „U“ gedrückt wird, minimiert sich das Fenster und bekommt den Titel „New Text Document“ in der Taskleiste.

Solche Boss-Tasten waren ziemlich populär in den 90ern, als man Spiele oft noch auf den Arbeitsrechnern zocken konnte.

Creep Night Resource Tool

Aus der Untersuchung ist ein kleines Python-Tool entstanden, das die Ressourcen selbständig lesen und die Dynamix-Bitmapdateien dekodieren kann.

Es kann unter anderem:

  • RESOURCE.MAP und RESOURCE.001 einlesen
  • OVERRIDE.DAT öffnen
  • enthaltene Ressourcen auflisten
  • Ressourcen extrahieren
  • die internen BMP:-Container analysieren
  • die 0x83-/Type-3-Kompression dekodieren
  • die RIFF-Paletten des Spiels lesen
  • Sprite-Frames als normale PNG-Dateien exportieren

Das Tool besteht ausschließlich aus Python-Code und enthält keine Originaldaten des Spiels. Und sicherheitshalber die Info: Teile des Tools wurden mit ChatGPT generiert.

Voraussetzungen

Benötigt wird lediglich Python 3.

Für die Analyse einer eigenen Installation benötigt man außerdem mindestens:

RESOURCE.MAP
RESOURCE.001

Beide Dateien befinden sich auf der Original-CD im Spielverzeichnis (im Ordner UPBALL2).

Am einfachsten legt man sie gemeinsam mit creepnight_tool.py in einen Ordner.

Ressourcen auflisten

Beispielsweise lassen sich alle Ghost-Ressourcen suchen:

python3 creepnight_tool.py list RESOURCE.MAP --pattern 'GHOST*.BMP'

Rohdaten extrahieren

Das gesamte Resource-Archiv kann folgendermaßen extrahiert werden:

python3 creepnight_tool.py extract RESOURCE.MAP -o extracted

Das Tool sucht automatisch nach RESOURCE.001 im selben Verzeichnis wie RESOURCE.MAP.

Liegt die Datei woanders, kann sie explizit angegeben werden:

python3 creepnight_tool.py extract RESOURCE.MAP \
  --data /pfad/zu/RESOURCE.001 \
  -o extracted

Einzelne Sprite-Ressource als PNG dekodieren

Die vier Frames aus GHOST0.BMP lassen sich beispielsweise so exportieren:

python3 creepnight_tool.py decode RESOURCE.MAP GHOST0.BMP \
  --palette CASTLE.PAL \
  -o decoded

Dabei wird standardmäßig Palette Index 0 transparent dargestellt.

Mehrere Ressourcen auf einmal dekodieren

Alle drei Ghost-Dateien können gemeinsam exportiert werden:

python3 creepnight_tool.py decode-all RESOURCE.MAP \
  --palette CASTLE.PAL \
  --pattern 'GHOST*.BMP' \
  -o ghosts

Für andere Spieltische können entsprechend deren Paletten verwendet werden, beispielsweise TOWER.PAL oder DUNGEON.PAL.

Wenn man einfach alle Dateien exportieren möchte, kann man --pattern einfach weglassen.

Vollständige Hintergründe exportieren

Bei Hintergrundgrafiken möchte man Palette Index 0 normalerweise nicht transparent darstellen.

Dafür gibt es:

python3 creepnight_tool.py decode RESOURCE.MAP CASTLE.BMP \
  --palette CASTLE.PAL \
  --transparent-index none \
  -o backgrounds

OVERRIDE.DAT untersuchen

Auch die Datei der „Large“-Installation kann direkt verarbeitet werden:

python3 creepnight_tool.py list OVERRIDE.DAT

bzw.:

python3 creepnight_tool.py extract OVERRIDE.DAT -o override-extracted

Unter Windows kann abhängig von der Python-Installation statt python3 einfach python verwendet werden.

Preservation und Urheberrecht

Das Tool selbst enthält keine Grafiken, Sounds, Programmdateien oder sonstigen Originaldaten des Spiels.

Das ist bewusst so gehalten.

Auch wenn alte Spiele häufig als „Abandonware“ bezeichnet werden, bedeutet dies nicht automatisch, dass ihre urheberrechtlich geschützten Daten frei verteilt werden dürfen.

Das Tool soll deshalb ausschließlich ermöglichen, die Ressourcen eurer eigenen Installation zu untersuchen.

Mehr als nur Nostalgie

Ursprünglich wollte ich einfach ein altes Pinball-Spiel auf Windows 98 starten.

Am Ende führte das zu einer Analyse des Dynamix-Ressourcenformats, einer rekonstruierten Bildkompression und einem eigenen Decoder für Grafikdateien, deren Format heute praktisch nicht mehr dokumentiert ist.

In RESOURCE.001 befinden sich 275 BMP:-Ressourcen mit insgesamt 6.833 einzelnen Frames. Der rekonstruierte Decoder konnte alle Frames auf exakt die jeweils im Header angegebene unkomprimierte Größe dekodieren.

So viel auch zum Thema Video Game Preservation:

Es reicht langfristig nicht unbedingt, nur eine CD oder ein ISO aufzubewahren. Wenn proprietäre Engines, Dateiformate und Werkzeuge verschwinden, gehen auch Informationen darüber verloren, wie diese Spiele technisch aufgebaut waren. Ein Format zu dokumentieren und einen kleinen unabhängigen Decoder dafür zu erhalten, ist deshalb ebenfalls eine Form der Bewahrung.

Die originalen Medien zu konservieren ist wichtig, aber das Wissen über proprietäre Dateiformate und Engines ist mindestens genauso wichtig. Was bringt es, einen Oldtimer zu konservieren und zu pflegen, wenn niemand weiß, wie der Motor funktioniert?

Und nebenbei ist es ziemlich faszinierend zu entdecken, mit welchen Tricks ein Spiel von 1996 damals versucht hat, „3-D Ultra“ zu wirken. Auch wenn es am Ende nur sehr viele kleine zweidimensionale Pixel waren.

Übrigens: Es gibt auch noch andere Spiele aus der „3-D Ultra“-Reihe, u.a. auch:

  • „3-D Ultra Pinball“ (das Original, sozusagen)
  • „3-D Ultra Pinball: Creep Night“ (das hier war also Nr. 2)
  • „3-D Ultra Pinball: Lost Continent“ (der dritte Teil)
  • „3-D Ultra Pinball: Thrillride“ (der vierte und letzte Teil)

Bis auf „Thrillride“ (auf Deutsch übrigens „Achterwahn“, autsch…) nutzen die ersten drei Titel wohl die selbe Engine. Vielleicht kann man mit dem Tool auch die anderen RESOURCE-Dateien auseinanderbasteln? Hm…


Ich habe alle diese Informationen und die Downloads, und noch ein bisschen mehr, auch noch auf eine eigene Infoseite ausgelagert (so wie ich das mit dem Spiel „Bad Day on The Midway“ von den Residents auch gemacht habe).

Diese findest du hier:
https://wegneronline.com/creepnight/

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert