Last time, we looked at a problem that turned out to be far easier for hacking a PC game than a console one. Today, we’re looking at a problem that is really only an issue for PC games.
These older game consoles tend to have specialized graphical hardware that works in terms of sprites and tiles, so their graphics are stored in a similar way from game to game. The PC-98 is a business computer that’s really good at displaying high-resolution text, but graphics-wise, programmers had to roll their own solutions for everything. As a result, pretty much every developer created and used their own proprietary format for storing graphics. E.V.O.’s developer, Almanic, used a format called .GDT.
E.V.O. has 9 title-card images composed entirely of Japanese text:
These don’t have any relevance to the gameplay – they’re not interface elements or anything. So, I wasn’t even sure it was necessary to edit them. A lot of projects will include non-essential image translations in the accompanying readme file. This seemed like a pretty safe option, but I wanted the additional polish of having it look like a full, legitimate English release. Plus, everyone said it would be really hard, so I wanted to try it.
When you boot up the game, as soon as you interrupt the opening cinematic, the first thing that shows up is TITLE1.GDT. This allows for some quick experimentation. You can take any other image and rename it to TITLE1.GDT, then see it as soon as you start the game.
To try to figure out the format, I tried to look at GAMEOVER.GDT, a tiny (0x233 bytes) and simple text image that’s mostly empty black void. I renamed it to TITLE1.GDT and stuck it in the disk.
From Bytes to Shapes
It seemed like the first thing to do was to open it up, delete large portions of the bytes from the end, and see what happens to the resulting image. Here are a few results of that, in reverse chronological order, with the length of the file at each point:
From this, you can get a general idea of what order the pixels get drawn. The screen fills in from left to right, in columns from top-to-bottom. In several images (100, 1A7, 1D4) you can see a hard edge after an 8-pixel-wide segment of a letter has been completed. So the columns are probably 8 pixels wide, written one line at a time.
Now, let’s focus in really close and try to highlight the exact byte when each individual line is drawn. For these, I’ve blown up the images by 400% and re-enabled scanlines, since E.V.O. only writes to every other line anyway. Here’s a little sequence where each byte appears to be responsible for one line each, in the middle of the M. Look really closely:
You can see the lower legs of the M being formed here, one line at a time. Now, what does the byte have to do with the line that is created? Let’s look really close at each line that’s created, in terms of which pixels are blue and which are not. We’ll call the blue ones “1” and the black ones “0”. Y’know, just as shorthand.
Well, I guess we chose the right shorthand – that’s exactly what is going on. A byte is 8 bits, and each bit here determines whether or not one pixel is turned on in blue.
From Bytes to Colors
That’s just one how one color gets drawn, though, and E.V.O. uses a palette of eight. The game reserves three areas of VRAM as the red, green, and blue color planes. The data in each color plane determines which pixels onscreen have a component of that color. With three components that can be either completely on or off, that means there are eight possible colors.
Can we use this to explain that hint of magenta from earlier?
At different stages in the rendering process, parts of the image go from black to blue, to magenta, to white. Magenta might not be a particularly significant color to you. But if we put this in terms of HTML color codes, the progression of colors goes like this:
#RRGGBB ------- Black: #000000 Blue: #0000FF Magenta: #FF00FF White: #FFFFFF
Each byte adds on another layer of color, not in RGB but in BRG (blue, red, green) order. Every column is drawn one color plane at a time, so at position 4D it has drawn the blue and red plane, and is in the middle of drawing the final green plane. That’s where the magenta comes from.
When to Ask for Help
At this point, I had the basics figured out, but I was missing a few parts. My attempts to write an encoder were producing results that weren’t quite right, and usually turned to garbage after a few lines. At SkyeWelse’s suggestion, I went to the Heroes of Legend forum, a place with a lot of Falcom translation projects going on, including last year’s Brandish 2 Renewal translation. A hacker named M_bot graciously took up the task the same day I posted it. He figured out a lot of the rest of the format. Here are the key parts I was missing:
- Whenever you want to write a byte pattern where the two nybbles are equal (like 00, 11, 22, … FF) the next byte determines how many times in a row to write that pattern. FF 03 writes “1111 1111” three times; 00 40 writes nothing sixty-four times. This is a popular simple compression method called run-length encoding (RLE). Since equal-nybble bytes (particularly 00 and FF) are much more common than different-nybble ones, this is a good compromise – you save space in the most common cases, but you don’t spend extra space writing “01” after every uncommon byte.
- The sequence “FF 8N” means to repeat the next binary write N times. So, there’s your way to repeat unusual sequences if really needed.
- .GDT uses several different encoding modes, which it can switch between at every new color plane. This is all just the mode introduced by the byte 04. M_bot and I each figured out a few of the other modes independently.
Once those were cleared up, there was enough information to write a much better encoder. M_bot quickly built one into his CompileTools package. It was meant to be a base, and only implemented the 04 encoding mode I’ve been describing so far. This was quite enough for encoding all of the chapter title cards, but there was one last pesky image to encode: AV04B.GDT, the title screen.
It’s the coolest and most prominent image, and the one most important to get right. When encoding it, it would get cut off towards the end:
I couldn’t figure out why. But at some point, I had all the .GDT images in a folder together, and sorted them by size. It turned out that AV04B.GDT was the largest image in the game, and it was 16 KB. And how big was my encoded version? 16.8 KB. What if the image is just too large? 16 KB is a nice, round (power of two) number, so maybe that’s how much space was allocated in memory for the image.
To shave off that last few hundred bytes, I implemented some other encoding modes which M_bot had figured out (mode 10, which copies the previous plane, and mode 8N, which copies the Nth previous block), but that still wasn’t quite enough. The original image was already letterboxed a bit, so I was able to crop the top and bottom of the image until it fit into 16 KB.
Anyway, that’s the dark secret of our E.V.O. logo – it’s oh-so-slightly cropped. But the additional letterboxing makes it even more cinematic, right?
Thanks again to M_bot who made the image encoding even possible! You can see the full encoder/decoder code is available as part of his CompileTools package on GitHub.
I spent at least a few months of the E.V.O. project time reverse-engineering this format, and I still couldn’t do it alone. But it’s not always a horrendous ordeal. In future articles I’ll write about some image formats that were at least as complex as .GDT, but not so much of a problem once I was a better reverse engineer.
Next week: Decompressing all the data in Rusty.