Red MoonShip battles

Three bugs and a button: the Blackbeard, seen from the wrong places

The first ship battle on PC put the camera inside a hull, lost Little Jack off screen, and left the world map staring up at its keel. Only three of those were bugs.

Log 045 min read

If you’ve played Skies of Arcadia, you remember the Blackbeard: Baltor’s ship, the first ship battle, and Drachma explaining what that “C!” at the top of the grid means. On the PC port it was where things got strange. Endymion played up to it and, as he put it, the glitches were “a sort of unintended peek behind the scenes”: the camera “frequently rendering from inside one of the ships”, the command menus showing sky and a floating island with no ships in sight, and, after the fight, a world map frozen on the view from underneath Little Jack.

Those looked like one camera problem. They turned out to be two unrelated bugs, a third that had crashed the battle earlier, and one button press that wasn’t a bug at all.

Making the battle last

The first thing I changed was how the battle was tested. The port’s --easy-battles cheat holds the enemy at 1 HP, so the first volley sinks the Blackbeard, which is great for getting past it and useless for testing it. I added a narrower cheat that keeps only the party’s HP and spirit full, so the fight runs its whole length.

It crashed on the S-Cannon turn. The effect number the game looked up was 1,546,321,920. Written in hex that’s 0x5C2B0000, and with its bytes reversed it’s 11,100: a perfectly sensible effect number, still in the GameCube’s byte order. (The GameCube stores numbers big end first; a PC reads them little end first, so the port swaps every number in the game’s data as it loads. Anything it doesn’t know is a number stays reversed.) Those 73 effect records are only ever reached through tables of pointers, so nothing in the decompiled code names them, and the loader never knew to swap them. Describing their layout fixed it.

The camera inside the ship

The battle’s camera isn’t a follow camera. It’s a recorded camera path, a “camera motion” stored in the battle’s model file in Sega’s Ninja format, the same family of formats the Dreamcast version used. The port converts Ninja models, textures and character motions to the PC’s byte order as it loads them, and warns about any chunk it doesn’t recognise. Camera motions were one of those.

So the camera motion’s length, 500 frames, read as about four billion. Every time the game asked “where is the camera at frame N?”, N was a tiny fraction of the motion, the lookup landed on its last key, and the eye ended up wherever that key sat, which was inside a hull. Converting the chunk put the camera back on its path.

Where did Little Jack go?

To check the fix I didn’t trust my eyes; I ran the same memory card and the same button presses in Dolphin, the GameCube emulator, and in the port, and compared the frames. The camera now matched exactly. The menu didn’t: Dolphin showed Little Jack small in front of the island, and the port showed the island alone.

Port, before the fixPort, before the fix
DolphinDolphin
The same moment of the battle from the same memory card and the same button presses. Drag the handle: Little Jack is missing from the port’s frame.

The ship’s position told the story. In Dolphin, Little Jack started the battle at (150, 25, 0). In the port it was at (300, 50, 0): exactly twice as far out, and just out of frame.

The game doesn’t move ships in a battle directly. It bakes each ship’s path ahead of time into a table of samples, one per frame, plus 120 extrapolated samples before the start and after the end so the ship never runs off the table. I logged the samples as they were made: 150, 25. Right. So I set a hardware watchpoint (the debugger stops the moment a chosen piece of memory changes) on the table’s first sample. It was written twice in one go: 150, then 300.

The second write was the padding step. Asked to pad a path with zero samples, it extrapolates from the first sample toward the second, which was never written and so still zero, and lands at twice the first. Then it copies that over the start.

Why zero samples? The path’s length comes from a list of route entries, 88-byte records of plain numbers. The decompiled code described two of those numbers as pointers. On the GameCube a pointer is 4 bytes, so that’s harmless; on a 64-bit PC a pointer is 8 bytes, which pushed the length 4 bytes later in the record (onto a word that holds 0) and stretched every record to 104 bytes. Describing them as numbers again put Little Jack back where Dolphin has it.

Port, after the fixPort, after the fix
DolphinDolphin
The same comparison once the route entries were read as numbers again.
A route entry is 88 bytes on the GameCube. With two fields declared as pointers, a 64-bit build makes each 8 bytes instead of 4, so the length field moves 4 bytes later and the record grows to 104 bytes. GAMECUBE · 88 BYTES numbers 4 length numbers 4 … AS THE PORT'S CODE SAW IT · 104 BYTES numbers 8 length? numbers 8 The data still has the GameCube's layout, so where the port looks for the length it finds the next word, which holds 0: a route with zero samples. (Schematic, not to scale.)
How two declared pointers moved the length field.

The world map that wasn’t stuck

That left the world map after the battle, staring up at Little Jack’s keel. I logged the camera every 50 frames and watched its view number change from the chase view to the view from below, between two samples where nothing else happened.

Except something did. The world map camera has preset views on the D-pad, and my replay file, written to click through the results screens, kept pressing Down after the map came back. Two presses of Up and the camera was back above the ship. The game was doing exactly what it was told.

Down: the view from belowDown: the view from below
Two presses of Up laterTwo presses of Up later
The “stuck” world map camera, and the same camera after two presses of Up. Both frames are from the port.

So: two byte-order bugs, one 64-bit pointer bug, and one button. The battle now plays from the first shot to Baltor’s retreat, and its opening frames match the GameCube’s.

Contact sheet of a replay runContact sheet of a replay run
A contact sheet from one of the replay runs of the battle, from the opening cutscene to the cannon exchanges.