Ein Kumpel hat mit einem Remake von Secret of Evermore angefangen, dem SNES-Rollenspiel von 1995. Ich wollte ihm nur schnell die Grafiken und Sounds aus dem ROM ziehen. Aus dem Nachmittag wurde ein Wochenende, an dem ich das halbe Spiel auseinandergenommen habe.
Das Spiel rendert für dich
Die Maps sind fies komprimiert. Ein LZSS-Layer für die Anordnung, ein zweiter Codec für die Platzierung, ein dritter fürs Tileset, dazu Paletten aus einer eigenen Tabelle. Ich hab die Codecs eine Weile von Hand rückwärts gebaut, Byte für Byte gegen einen RAM-Dump. Ging, war aber zäh.
Dann kam mir die Idee, die Arbeit gar nicht selbst zu machen. Das Spiel kann seine Maps ja rendern, es tut nichts anderes. Also lade ich es headless im Emulator (Mesen2 über Lua) und häng mich an die spieleigene Map-Load-Routine. Ein Callback auf die Stelle, wo der Loader die aktuelle Map-ID aus dem RAM liest, und kurz davor überschreibe ich diese ID mit der, die ich sehen will.
emu.addMemoryCallback(function() emu.write(0x0ADB, target_map, emu.memType.snesWorkRam) end, emu.memCallbackType.exec, 0x908F68)
Das Original baut dann jede Map über seinen kompletten eigenen Pfad, und ich lese am Ende einfach VRAM und CGRAM aus. Der Extractor bootet Mesen2, setzt die Map-ID und dumpt den dekodierten Zustand:
map 16: target=16 mapnum=16 24x15 -> OK, painted=104764 missing_quads=0 (2.9s) map 54: target=54 mapnum=54 25x25 -> OK, painted=238854 missing_quads=0 (2.7s) map 105: target=105 mapnum=105 54x79 -> OK, painted=1239641 missing_quads=0 (3.0s)
target==mapnum heißt, das Spiel hat genau die Map geladen, die ich wollte, und kein einziger Tile-Quadrant fehlt. So sieht eine fertige aus, die Kathedrale mit ihren Glasfenstern:

Das läuft über alle 127 Maps, kein einziger Fehler. Hier alle auf einen Blick, direkt aus dem Spiel gezogen:

Die erste Dschungel-Karte
Weil die Maps aus dem laufenden Spiel kommen, laufen sie auch weiter, wenn ich sie einfach stehen lasse. Damit fallen die Animationen quasi ab: das Laub schimmert, das Wasser kräuselt sich, die Fackeln flackern. Hier der Dschungel mit den geschlängelten Wegen, in dem der Junge am Anfang herumrennt, direkt aus dem Spiel als GIF:

Und die Vielfalt ist ordentlich. Vom Marktplatz über die Junk Towers bis zu den Lava-Pfaden, wo die Lava wirklich fließt:




Karten selbst würfeln
Wenn ich eh die echten Tiles und die Nachbarschaften jeder Map habe, kann ich damit auch neue Karten bauen. Ich hab die Adjazenz-Regeln aus den Original-Maps gelernt und per Wave Function Collapse frische Karten generiert, mit dem echten Tileset gerendert. Die Glashalle liefert eine schöne Fenster-Grammatik:

Bei anderen Vorlagen kommt eher Mauerwerk und Boden raus:

Ehrlich: für den Dschungel sind die WFC-Regeln zu schwach, das Biom flutet monoton und sieht lange nicht so gut aus wie die echte Karte mit ihren Wegen. Darum läuft mein Remake auf der echten Map, WFC bleibt der Knopf zum Durchwürfeln.
Wenn es nicht klappt, ehrlich bleiben
Nicht alles lief so glatt. Der Junge, also der Spieler-Sprite, liegt als rohes 4bpp im ROM, den konnte ich pixelgenau gegen ein Referenz-Sheet prüfen. Beim Versuch, auch die Angriffs- und Zauber-Posen so zu ziehen, bin ich abgeblitzt.
Derselbe Abgleich, der Stehen und Laufen exakt trifft, kommt bei den Action-Posen nur auf 0.44 bis 0.79. Zusammengesetzt sieht das aus wie Tile-Salat, lauter falsch zusammengesuchte Kacheln. Der Grund ist dass die dynamischen Posen nicht als fertige Bilder im ROM liegen. Sie werden zur Laufzeit aus einzelnen Hardware-Sprites mit eigenem Versatz zusammengebaut, gesteuert von einer Script-Engine. Ich hab das Negativ so stehen lassen, statt erfundene Offsets als fertig zu verkaufen.
Ein spielbares Stück
Am Ende hab ich ein pygame-Remake mit den echten ROM-Daten. Der Junge läuft und rennt in alle Richtungen mit den echten Frames, auf der echten Dschungel-Karte mit ihren Wegen, Items, einem NPC und echten Dialogen aus dem Spiel:

Für Angriff und Zauber mach ich es wie das Original: der Körper bleibt fast statisch, die Waffe und der Zauber sitzen als eigenes Sprite obendrauf. Genau diese Effekt-Sprites hab ich aus dem ROM geschnitten:


Dazu ein Menü mit den echten Item- und Alchemie-Tabellen:

Das ist alles noch Work in Progress, ich bau weiter dran. Man sieht ja auch unschwer, dass die Animationen noch nicht sauber exportiert werden, die GIFs flackern und ruckeln an den Raendern. Das kommt noch.
Ping-Pong mit der KI
Das Hin und Her war fast interessanter als der Code. Die eigentliche Fleißarbeit hat Claude gemacht, das Codec-Rückwärtsbauen, das Sprite-Matchen, verteilt über viele parallele Agenten. Die Ideen, wo es langgeht, kamen aber im Gespräch.
Der Caller-Trick zum Beispiel. Claude hat erst brav die Codecs von Hand geknackt, Byte für Byte. Ich hab dann gesagt, lass doch den Emulator die Funktion für dich aufrufen, das Spiel rendert seine Maps doch selbst. Das war der Moment, wo es klick gemacht hat, und ab da liefen alle 127 Maps.
Der andere Dauerbrenner war Ehrlichkeit. Claude hat mir mehr als einmal eine saubere Kathedrale verkauft, während das Bild noch dunkel und kaputt war. Also hab ich drauf bestanden, jedes gerenderte Bild selbst zu sehen, bevor irgendwas als fertig gilt. Genau so sind mehrere angebliche Durchbrüche als noch nicht fertig aufgeflogen. Beim Spieler-Sprite, wo die Action-Posen partout nicht aus dem ROM wollten, war die Ansage: lieber ein ehrliches Negativ als erfundene Offsets. Und bei den Effekten hab ich entschieden, es wie das Original zu machen, statt tief in die Animations-Engine zu graben.
Ich hatte die Ideen und den Blick fürs Ergebnis, Claude die Ausdauer und die tausend Details. Zusammen war das an einem Wochenende durch, allein hätte ich Wochen gebraucht. Nichts von Square landet übrigens im Repo, alle Assets kommen aus der eigenen ROM und sind gitignored.