DEV Community

Cover image for Translating a Japanese Sega Saturn Game From 1998: The User Interface
Joe Buckle
Joe Buckle

Posted on

Translating a Japanese Sega Saturn Game From 1998: The User Interface

This is part 2 of a 3 part series, probably...

Part Title
1 Translating a 1998 Japanese Visual Novel on Sega Saturn I Bought by Accident
2 Translating a Japanese Sega Saturn Game From 1998: The User Interface
3 ...incoming

It is worth noting up front that without AI I don't think this project would be possible for me. It has been instrumental in discovering the source code and figuring out where all this stuff goes.

Part 1 ended with a Sega Saturn in the corner of the room playing a Japanese visual novel in English.

The dialogue worked. That was the important milestone. The characters were speaking English, the script was extracted, translated, rebuilt, and running on the original hardware.

But of course when I tried to play it the dialogue was English but everything around it was still Japanese.

Menus. Locations. Profile cards. The save screen - the parts of the game that tell you what to do.

The UI was about to be a very different problem.


The text that isn't in the script

The dialogue was relatively straightforward because it lived in a place designed for dialogue.

This wasn't the case for the user interface.

The menus and labels are stored inside the game's executable as raw strings. They are not in the script files, and they are not stored in a nice, editable format.

Each string sits in a fixed space, with the next string immediately after it.

The UI was built around fixed Japanese labels, with no extra space reserved for longer text in another language, so English strings simply overflow into whatever is next to them.

アルバイト

fits perfectly.

Part-time Job

does not.

The extra bytes have to come from somewhere.

Garbled, overlapping English text on the Yokohama map - what overflow actually looks like on screen

This is what it looks like when it goes wrong. That label should say one thing. It is drawing several things at once, on top of each other.


The first UI mystery

Luckily, some English text already looked better than expected.

If you read the last article, you'll know the dialogue renderer needed a patch because every character was treated as the same width.

The UI was different.

Some labels were compressed. Others were spaced out.

The schedule label before and after: PLANS is spaced out wide while the date panel beside it draws narrow

Look at the English side: P L A N S is stretched across the wide spacing, while the date next to it - 3/20/Th 18h - is packed tight. Same screen, two widths.

The game actually contains two versions of its font. A wide one at 24 pixels for Japanese, and a narrow one at 16 pixels that covers everything that isn't a kanji, including the full Latin alphabet.

The renderer automatically chooses between them depending on the character being drawn.

This was something the original developers had already solved in 1998!

They built an interface that could mix Japanese and English characters, even though English was never going to be the primary language. Lucky us!


When English exposes old assumptions

The problems started appearing when English got longer.

One menu centres its text using a simple calculation:

x = (96 - advance * length) / 2 + 4;
Enter fullscreen mode Exit fullscreen mode

96 pixels of usable width, divide the leftover space, nudge it right by 4. Perfectly sensible.

Then you give it a seven character English word:

// "PROFILE" -> 7 characters at 24 pixels each
x = (96 - 168) / 2 + 4;
// x = -32
Enter fullscreen mode Exit fullscreen mode

There is no safety check, I'm guessing because there never needed to be. No Japanese label was ever long enough to go negative. Mine were, immediately, and the label starts drawing outside its own area.

The travel map title overflowing its box, spilling over stray characters

That title plate is meant to say "Travel Map". The text has escaped the box and collided with whatever was already on screen.

So rather than making this a complicated rom hack, I decided to get Claude to change the words so they still made sense but fit the rules the original game expected.

STATUS survives. PROFILE does not.


The plan was simple

Most UI strings have pointers - so the obvious solution would be to move the longer English string elsewhere and update the pointer.

This seems to be a common technique in reverse engineering.

And mostly, it worked. Locations, menu entries, options, 64 profile fields. All moved without complaint.

Then the schedule screen locked the game solid.

I got this wrong three times. I blamed the line length, then the memory region I was writing into, then the renderer. What I should have done first was build a version with no UI changes at all, to see whether the screen still worked.

It worked perfectly. So the fault was my text, not the game.

From there it was just bisection. Hold back the whole screen, works. Put the labels back, works. Put the nine job messages back but leave them exactly where they were, works.

Put them back and let them move, locks.

The pointers were never the problem. They all existed, and they were all real. The problem is that the code also reads forward from one message to the next:

char *p = pool[MSG_BASE];
for (int i = 0; i < 9; i++) {
    draw(p);
    p += strlen(p) + 1;   // walk to the next one
}
Enter fullscreen mode Exit fullscreen mode

So message 4 has to physically sit directly after message 3. Move any of them somewhere roomier and the walk carries on into whatever bytes happen to be sitting there instead.

RELOCATABLE                      BLOCK-WALKED
  ptr --> "PLANS"                  base --> "msg 1"
  ptr --> "TRAVEL"                          "msg 2"   must follow
  ptr --> "PHONE"                           "msg 3"   must follow

  move it, rewrite the pointer     move one, break every one after it
Enter fullscreen mode Exit fullscreen mode

Having a pointer wasn't enough. Some of these strings are addressed twice, in two different ways, and only one of them is visible to a tool that scans for pointers.

The rule I ended up with: message blocks stay where they are. Fit the English to the space rather than moving the space.


The padding that wasn't padding

The executable appeared to contain spare space. There were gaps between strings filled with zeros which looked like enough space to fit the extra English.

But it turns out the Saturn's processor expects certain data to be aligned correctly in memory.

1,319 of the 1,321 strings begin on 4 byte boundaries. Those gaps are there to maintain alignment.

Removing them causes the game to crash to a black screen.

Three times, before I worked out what I was looking at.


Finding space that isn't there

Looking back at the executable it appeared to have large sections of empty-looking data. Surely some of that must be unused?

Not necessarily.

Some areas are tables that are only ever read, so nothing writes to them and they look dead from the outside. Others are buffers belonging to one screen. One region I was confident about turned out to be the save screen's working memory, which I had simply never opened while testing.

A region can look empty until the exact moment the game needs it.

Proving something is unused is a much harder problem than it sounds.

There was no reliable free space :(


Creating space instead

The executable loads into memory just below the stack, and the stack grows downwards. Between them is a small gap.

After measuring normal gameplay across a couple of sessions, the stack never came closer than about 1.8 KB. I extended the executable into that area, keeping 512 bytes clear as a margin, which gave 1,344 bytes of extra room.

It is not a perfect solution, but the failure mode is better.

If I accidentally overwrite game data, the result could be anything.

If the stack ever reaches the new strings, the worst case is broken text.


Saving bytes with words

The obvious space saving exercise would be modifying the translation itself. Some punctuation was removed, long labels were shortened, and words were changed to fit the available space.

Save to this file?

became:

Save here?

長崎駅 is Nagasaki Station. It became:

Nagasaki

None of these changes mattered individually, but together they made the interface fit.


The screens that aren't menus

A menu is the easy case. Most of the interface isn't one.

This little window caused me more trouble than any menu. The script has location labels in it, I had translated all of them, and the window was still Japanese. There are two completely separate sets of place names in this game: the ones in the script, and a second list of 160 inside the executable. The window uses the second one. So "the labels are 100% translated" was perfectly true and completely useless.

The travel map before and after: 金沢着 20日18時 becomes Kanazawa 20d 18h

The calendar and travel panels were the opposite, and a genuine relief. They aren't sentences at all, they're assembled from pieces at runtime: place name plus 着 for an arrival, day plus 日, hour plus 時. Translate the small joining pieces and the English just appends itself. A rare case of a 1998 design decision doing me a favour.

The diary calendar in English

Now - I know the settings page isn't perfect, but it does the job.


The limits you cannot negotiate

Some parts of the UI cannot be expanded.

Character names are stored in fixed size records, 26 bytes each, and the name is only the first 7 of them:

82 D9 82 CC 82 A9 00   ほのか + terminator   <- the name slot
02 BC 0A 04 2F         stats
00 00 00 00 00         padding
54 41 45 00            "TAE"                  <- voice stream tag
88 C0 92 42 00         安達                    <- surname
Enter fullscreen mode Exit fullscreen mode

The space immediately after the name is live game data. Making the name field larger would overwrite her stats.

And these particular strings have no pointer anywhere. The game works out their address by arithmetic, so there is nothing to update and nowhere to move them to.

So that leaves 6 usable bytes. Japanese text is 2 bytes per character, which the renderer requires even for English.

Six divided by two.

Hon.

Tae.

Emi.

Three letters wasn't a style decision. It was long division.


The finished interface

After all of that, the game became playable without knowing Japanese.

  • The menus work.
  • The locations are readable.
  • The profiles make sense.
  • The save screen no longer requires guessing.

This was a different kind of translation.

The dialogue was about language, the UI was about archaeology.

Finding out what assumptions the original developers made, and then trying to work within them.


If you want to try this yourself

The latest version of the GitHub repo is here:

https://github.com/joebuckle-dev/sentimental-graffiti-translation-claude


The part I had not counted on

Now then, the script, executable, fonts and graphics are only part of the disc.

The rest is audio.

I wasn't going to bother with the he voices. But I am quite a ways down this rabbit hole now and I'm wondering if I'd see light by at least attempting to get AI to do these voices for me, with my English text.

Literally hundreds of megabytes of voices. 426 of them, to be exact, which is very nearly three quarters of the disc.

Twelve characters and over twelve hours of spoken Japanese.

That's next...

Top comments (0)