Skip to content
Michael Malura
273 GitHub stars 10060 npm downloads 29 Projects

Secret of Evermore: The Boy Now Comes Entirely From the ROM

In the first part I let the game render its own maps and had to give up on the boy's sprites. Standing and walking worked by pixel-matching against a reference sheet, but running, attacking and casting only reached 0.4 to 0.8 and looked like tile salad. My explanation back then: the dynamic poses are not stored as finished images in the ROM, they are assembled at runtime from individual hardware sprites. That was true. What was missing was the path to them.

That path is found now. Every frame of the controlled boy comes out of the ROM, with the game's real timing, and is checked pixel by pixel against the emulator.

Kontaktbogen: alle 60 Frames des Jungen aus dem ROM, Stehen, Gehen, Rennen, Angriff in vier Richtungen
Kontaktbogen: alle 60 Frames des Jungen aus dem ROM, Stehen, Gehen, Rennen, Angriff in vier Richtungen

The wrong lead

The previous session had left a handoff that sounded quite certain: the sprite composer in the ROM has several entry points, and the walking boy is rendered through one of them, one the existing trace had never seen. Three concrete addresses with their callers.

Two of the three were nonsense. They sat in the middle of instructions, one on a TXA, one on a REP. The supposed callers lived in a bank that could not be read as code under any flag combination, so data that happened to look like a jump. A byte scan for opcodes finds this kind of thing all the time, which is exactly why the rule for this round was: believe nothing that has not been disassembled or measured.

The third address was real, and it resolved everything. It is not a new entry but a re-entry into the same routine, a few bytes past its start. It is called by a loop that walks a linked list every frame: nodes sorted by screen height, one per visible sprite, each with position, palette and a pointer to its metasprite. The old trace sat on the start of the routine and therefore missed every node in that list, the boy included. A few bytes off, a whole session lost.

The chain

From there it went link by link backwards. Where does the pointer in the list node come from? From two fields of the player entity, one per layer. Who writes those fields? A small bytecode machine in bank $D0 that runs an animation script per entity. And the scripts sit in bank $C8, directly readable once you know the opcodes:

52                script start, resets fields
4D 04 00          speed 4 (running: 12)
22 4B 11  08      metasprite $CB114B, then wait 8 frames
C2 06             move one step
22 65 11  08      next metasprite, 8 frames
...
D3                loop back to start

The byte after the metasprite is the display duration in frames. The emulator measured 8, 8, 7, 8, 8, 6 frames for walking up, and those exact numbers are in the script. The last frame lasts one tick longer in the game because the loop jump itself costs a frame. Both are in the manifest now.

Which script is running is decided by a table in bank $C4: 16 bytes per action, four directions of four bytes each. Group 5 is standing, 6 walking, 7 running, 10 attacking. The direction order is up, right, down, left.

Gehzyklus in vier Richtungen mit dem Timing aus dem Animations-Script
Gehzyklus in vier Richtungen mit dem Timing aus dem Animations-Script

Why the images were garbage at first

With the scripts I had the metasprites, but the blocks inside rendered as nonsense. The block numbers were far too small for the known graphics table. I first guessed at an offset. There was none. The composer simply has two tables: a flag bit in the chunk decides whether a block is a 16×16 piece from one table or an 8×8 piece from the other. The boy mixes both, torso in 16×16, hands and feet in 8×8. If you only know one table, you see half the figure and take the rest for broken.

Then the colours. The palette from the character record had a garish green in two slots, and hair and clothes did not match the game. The answer came from the emulator: the palette sitting in colour memory at runtime exists word for word in the ROM, just three palettes further on. There are eight variants of the boy, and the two green slots are the weapon colours. Palette 0 is the placeholder without a weapon, palette 3 the boy with the bone club from the jungle.

Finally the order. The standing poses were at 0.87, every deviation on the seam between hips and torso. On the SNES a sprite with a lower OAM number is drawn in front, and the composer writes the first chunk into the lowest slot. My renderer drew the first chunk first and thereby put it behind. Reverse the loop once, all standing poses at 1.0.

Pixelvergleich: Atlas-Frame, Rekonstruktion aus dem Emulator und Differenz, Score 1.0
Pixelvergleich: Atlas-Frame, Rekonstruktion aus dem Emulator und Differenz, Score 1.0

Running needs the ring

The walking frames could be measured because I load a savestate into the emulator via Lua and send the boy direction keys. Running did not work. I first thought B was the key, but B is attack, which at least gave me the attack frames. Running is A, and only with the Jaguar Ring, which my savestate did not have.

Setting the script cursor directly to the run script achieved nothing, the game resets it the next frame. So the other way round: according to the RAM map the charm flags live in three bytes. All three to FF, hold A, and the boy runs, exactly with the frames from the script. Then bisect, first by byte, then by bit: bit 1 in $7E2262 is the Jaguar Ring.

Rennzyklus in vier Richtungen, im Emulator erst mit Jaguar-Ring erreichbar
Rennzyklus in vier Richtungen, im Emulator erst mit Jaguar-Ring erreichbar
Angriff mit der Knochenkeule in vier Richtungen
Angriff mit der Knochenkeule in vier Richtungen

Verify instead of claim

The verification path is the same as last time, only stricter. A Lua script records OAM, video and colour memory every frame while the boy moves. From that the boy is reconstructed the way the console drew him and laid over the frame from my atlas that carries the same metasprite. The score is the share of exactly identical pixels.

Of 60 frames, 46 have a counterpart in the recordings. 44 of them sit at exactly 1.0. The other two are isolated single frames in the capture where the console happens to show more sprites than the pose has, probably the swing effect or the flicker against a wall. They are marked open in the manifest instead of being smoothed away.

How it went

The process was different from the first part. I set the direction and looked at the results, Claude spread the work across parallel agents: one wrote its own small disassembler because Ghidra keeps guessing the register widths wrong in this code, one drove Mesen with savestates, one compared pixels. Reports came with bytes and addresses, and every claim was cross-checked with a measurement before it was allowed into the docs.

My part was the view from outside. I saw in the emulator window that a plant hit the boy while the trace was running. Without that hint the sequence would have been evaluated with hit frames in it. And the instruction that compiling, tracing and testing can run on smaller models in many parallel agents let the whole thing finish in one evening instead of getting stuck in a single long session.

What is missing: the remaining action groups of the table, so casting, damage, death, and the dog, which follows the same pattern in a neighbouring bank. The parser is the same. As last time nothing from Square ends up in the repo, the atlas is rebuilt from your own ROM.

Share X Reddit Hacker News LinkedIn E-Mail
Subscribe to new posts RSS JSON