r/WonderSwan • • 15h ago

[How it's made] How an AI translated and ROM-hacked a WonderSwan Color game, step by step: Blue Wing Blitz (long, technical)

0 Upvotes

A few days ago I posted an English patch for Blue Wing Blitz, Square's 2001 air-combat strategy game for the WonderSwan Color. Several people asked how it was made, so this is the long version. I've tried to write it so that a programmer who has never touched a ROM hack can follow the whole process and reuse the ideas on another game.

Please read first: the translation and all the hacking were done entirely with AI (Claude), as a personal project. I directed the work, played the builds and reported what was wrong; Claude did the reverse engineering, the code and the English. I share the patches for personal use, but they must never be uploaded to romhacking sites: they aren't up to the standard of a real fan translation. This post is about the method, not a claim that AI replaces human translators and hackers.

Contents

  1. The machine in five minutes
  2. The workbench
  3. Finding the font, then the text
  4. How the game prints a line
  5. Translating with context
  6. A new font
  7. Making longer English fit: jump stubs
  8. The page engine: wrapping, new windows and the tile ring
  9. Tables the game copies around
  10. Compressed graphics
  11. Testing
  12. The double AI review
  13. Lessons for programmers

1. The machine in five minutes

  • CPU: NEC V30MZ, a fast 80186-compatible 16-bit x86 at about 3 MHz. The usual x86 segment:offset addressing applies: physical address = segment × 16 + offset, 20 bits, so 1 MB of address space.
  • Screen: 224 × 144. The video hardware works with 8×8 tiles, 2 bits per pixel (4 colours per palette) in the mode this game uses.
  • Memory map (simplified):
    • segment 0000: internal RAM (work RAM, tile data, tile maps, palettes);
    • 1000: cartridge SRAM (saves);
    • 2000 and 3000: two 64 KB windows onto any ROM bank, chosen with I/O ports 0xC2 and 0xC3;
    • 4000–FFFF: "linear" ROM, chosen in 1 MB blocks with port 0xC0.
  • This game's layout: the ROM is 2 MB, i.e. 32 banks of 64 KB. With the setting the game uses, segments 4000–FFFF read the upper half of the file directly. So segment 7000 = file offset 0x170000, D000 = 0x1D0000, and F000 = 0x1F0000, the last bank, which holds the main code.
    • Everything below is written as segment:offset (what the CPU sees) or as a 6-digit file offset (what you patch).
  • Header: the last 16 bytes of the ROM hold the header. The last two bytes are a 16-bit sum of every other byte. Recompute it after patching, or emulators will complain.

2. The workbench

Everything ran in a Linux sandbox that Claude drives by itself:

  • Python for every tool: extractors, encoders, compressors, the build script.
  • MAME, run headless and scripted in Lua: press buttons on given frames, take a screenshot after every step, read or watch memory.
  • A reproducible build. python3 src/build.py starts from the untouched Japanese ROM and applies everything: font, text, code, tables and graphics.
    • Nobody ever edits the ROM by hand.
    • Every patch first asserts the original bytes it expects to find (assert rom[a:b] == ...). If a guess about the code is wrong, the build stops instead of silently corrupting the game.
  • BPS output, made by a small Python BPS encoder.

My part was to play, send screenshots of anything wrong ("this window overflows", "this name is cut", "Japanese left in this menu"), and decide on names and taste.

3. Finding the font, then the text

There was no existing table for this game, so the first job is to learn how the game encodes characters.

The font. A tile viewer over the ROM (2bpp, various widths) shows the font sheet at file offset 0x120000:

  • 16×16 glyphs, each made of four 8×8 2bpp tiles;
  • 8 glyphs per 0x200-byte row, laid out like this:

Kana and kanji.

  • The first glyphs are a space, digits, punctuation, then hiragana and katakana in the standard order, so the kana part of the table can be read off directly.
  • After them come about 380 kanji in no particular order, plus icons. Those were identified with a small template-matching OCR:
    1. Render every kanji of JIS X 0208 (about 6,300) with Noto Sans CJK.
    2. Crop each rendering and each game glyph to its ink, centre it in a square, scale it to 24×24 and blur it slightly.
    3. Turn each into a zero-mean, unit-length vector and take the best cosine match.
    4. Fix the misreads by context, once the text around each kanji is readable. Small kanji at 12 px look a lot alike: 未 vs 末, 土 vs 士.

The encoding.

    0x30-0xFF     one byte  = glyph (code - 0x30)
    0x21 xx       two bytes = glyph 0xD0 + xx
    0x22 xx       two bytes = glyph 0x1D0 + xx
    0x00-0x1F     control codes
    0x01          end of line (return to the caller)

Finding every line.

  • A scanner walks the script banks looking for byte runs that decode to plausible Japanese: valid glyph codes, lots of hiragana, ending in 0x01. It scores each candidate.
  • Candidates alone are not proof, so the scanner also cross-references them. The event script calls each line with a 3-byte instruction 04 lo hi (call hi:lo in the same segment), so a real line is one that some 04 points at.
  • That gave a dump of about 2,000 lines with their exact address, length and call sites (refs.json). The call sites matter later.

4. How the game prints a line

This is a real window from the story, as bytes in the event script (bank 0x1B):

    04 13 E2      call 1BE213   "われわれは まずロギーナ軍を"  (We must first...)
    0F 00         new row
    04 23 E2      call 1BE223   "おいださなくてはならない"     (...drive out the Roggina army)
    04 2C 6E      call 1B6E2C   wait for a key (a small routine)
    0F 01         clear the window

And the first line itself:

    6B 69 6B 69 59 30 5E 7F BB C9 77 A5 21 4E 6C 01
    わ れ わ れ は ␣  ま ず ロ ギ ー ナ 軍(2 bytes) を  end

Other control codes worth knowing:

  • 0F 06 nn moves to column nn.
  • 0F 04 waits for a key inline.
  • 07 49 lo hi 04 00 03 prints a name stored in RAM at hi:lo: the eight pilots live at 0B00, 0B10 ... 0B70 and the airship at 0BF0, because the player can rename them.
  • A "speaker header" is a small subroutine that prints the name and a full-width colon.

From this the extractor groups lines into pages. A page is what one window shows between two waits: the lines, how many rows they use, the speaker, the column, the kind of window and what ends it. Blue Wing Blitz has 1,431 pages. Translating page by page, not line by line, is what makes the English read as speech instead of as fragments.

5. Translating with context

The English script is a plain text file keyed by ROM address. One entry can cover a whole page (+1 = the page spans one more line):

    1BE213+1    First we must drive out the Roggina army.

Names are tags, {Keid}, {Payer} ... {Ship}, because the player can rename the heroes and the airship.

Rules Claude followed:

  • Source text. Translate from the Japanese in the ROM; there was no earlier English translation of this game to lean on.
  • Register. The Japanese is mostly hiragana with few kanji, written for a young audience. The English should be simple and lively but not childish. Menus, briefings and aircraft profiles use real aviation terms.
  • Names. Use official romanizations where they exist: Square's 2001 announcements as reported by RPGamer, the English Wikipedia article, and the official PlayOnline site through the Wayback Machine. The aircraft keep the Norse, Slavic and Celtic myths they are named after: Hraesvelg, Gullinbursti, Svantovit, Triglav...
  • Leftovers. Translate them too, even if almost nobody will see them: the debug sound-test strings, and 70 developer placeholder profiles ("プロフィールC / ここにプロフィールが / いろいろでます" → "Profile C / Profile text / goes here.").

6. A new font

The game was already proportional. The text engine reads a one-byte advance width per glyph from a table at 0x1DEB74. This saved a lot of work: no new renderer was needed, only new glyphs and new widths.

  • Base font: Shinonome 6x12, a public-domain bitmap font loaded from its BDF file.
  • Redrawn in the game's style into the 16×16 cells:
    • ink in colour 3;
    • a drop shadow in colour 2 one pixel right and down;
    • background in colour 1;
    • baseline aligned with the Japanese digits;
    • advance = ink width + 2 (shadow column + 1 px gap).
  • Glyph slots:
    • The Japanese no longer needs its hiragana slots, so lowercase letters and punctuation went there. The one-byte codes 0x40–0x59 are now a–z.
    • The capitals use the game's existing Latin slots.
    • Digits and the two-byte icons are kept as they are: HP, fuel, missile counts and stat bars, written in the script as {HP}, {GAS}...
  • Encoder: a small charmap.py turns English plus tags into bytes. | = new row, ^ = wait and clear, {XX} = raw control byte.

7. Making longer English fit: jump stubs

This is the central trick, and it works for many games with a bytecode script.

The problem.

  • English is longer than Japanese, usually 1.5–2× in bytes here.
  • Each line is called by a 16-bit pointer from the script, inside its own 64 KB segment.
  • The script banks are full. Moving every line would mean finding and rewriting every pointer, and some of them hide inside code.

The solution.

  • Leave every pointer where it is. If the English fits in the Japanese line's bytes, write it in place (332 lines).
  • Otherwise, overwrite the first three bytes of the Japanese line with a jump to the English, stored in free ROM somewhere else.
  • The text interpreter has no such jump, so new control codes were added. The interpreter dispatches control codes 0x00–0x1F through a table of near pointers at F000:D2EB, and several entries are unused.
    • To find that table: Lua read taps in MAME show which code reads the text bytes, and from there the interpreter loop and its jump table are a short disassembly away.
  • Claude put small handlers in free space at F000:E600 and pointed the unused entries at them:

The whole 0x13 handler is five instructions. si points at the control byte and es:di is the interpreter's context:

    mov si,[si+1]          ; new text pointer = the 2 bytes after the code
    mov ax,0x7000
    mov ds,ax              ; text now comes from segment 7000
    mov [es:di+0x83],ds    ; store it in the context, so it survives
    ret

Why this works.

  • The English ends with the same 0x01 as the Japanese. The interpreter keeps its own call stack in that context: depth at +3, 4-byte entries (offset, segment) from +0x85. So 0x01 simply returns to the script, exactly as the original line would have.
  • The script never knows anything changed. The window from section 4 now starts with 13 E7 6B, a jump to 7000:6BE7, where "First we must drive out the" is stored.

Two corner cases.

  • Lines shorter than three bytes can't hold a jump. For those, the build switches each caller from 04 lo hi to 15 lo hi, a far call into a copy in bank 0x1D. This is why the call sites were collected in section 3.
  • Wait routines. Relocated text sometimes needs to call the wait routine of its original bank. 0x02 calls a routine in the segment saved on top of the call stack, so moved text can still use the right one.

Space allocation. A simple first-fit allocator manages:

  • the free areas of each bank;
  • the "gaps": the tails of Japanese lines that became 3-byte jumps, plus space freed by moved blocks.

Pointer tables that must stay in their own segment (section 9) are fed from those gaps.

Final count: 332 lines in place, 1,586 relocated, 65 moved out through far calls, and about 69 KB of ROM still free.

8. The page engine: wrapping, new windows and the tile ring

Writing the bytes is the easy half. Making every page look right is where most of the time went.

Wrapping.

  • Text windows are 13 cells (208 px) wide and show two rows. Each page knows its kind of window, so the usable width is computed per page (for example, minus the column offset).
  • Greedy word wrap by pixel width, using the exact advance table of the new font.
  • Player names are counted at their maximum width (7 letters), because the player may rename Keid "WWWWWWW".

More rows than the Japanese.

  • The extra rows are distributed over the page's lines, with 0F 00 new rows between them.
  • When the window is full, the engine inserts a page break: the same wait routine the script uses on that page, then 0F 01 clear. Long speeches therefore continue in a new window, just as the game does by itself.

Names. The script's own name insertions inside a translated page are switched off by changing their last byte 03 into 05, a 1-byte no-op. The English carries its own insertions wherever the name falls in the English sentence.

The tile ring. This was the nastiest bug.

  • A text window doesn't draw into a fixed area. It renders glyphs into a ring of 80–96 tiles, which is about 320–384 px of text per page.
  • If a page needs more, the ring wraps and the last glyphs overwrite the first ones.
  • Japanese never came close to the limit; English did.
  • The build now simulates the renderer's tile counter for every page, including the speaker header, and adds a page break wherever the ring would overflow.

The column-jump patch. The original engine charged a column jump at the start of a row as if the skipped pixels had been drawn, wasting part of the ring on every indented row. A 4-byte patch at F000:C6DC calls a 17-byte routine that charges only x & 15.

Checks.

  • check.py reports any row wider than its window.
  • flowstat.py counts pages that don't fit; it must report 0 before a release.
  • The speaker headers' full-width colon was replaced by a plain :.

9. Tables the game copies around

Some strings aren't printed by the script. They are data:

  • default pilot names;
  • aircraft nicknames;
  • model numbers;
  • airship upgrade messages.

These are read through 16-bit pointer tables in their own segment, and some are copied one after the other into the save data by a block-copy instruction of the script. The game assumes the strings sit back to back.

  • Rebuilding them. The build writes a new block and a new pointer table in free space of the same bank, then patches the script operands that point to them.
  • The 7-glyph limit. The name-entry screen keeps at most 7 glyphs, the same field the player types into.
    • Long default names like "Discovery" use ligatures: two narrow letters drawn in one 16×16 glyph, stored in unused kanji slots (21 per table).
    • Model numbers are shortened: アリウス → "Ar.", ラムヤ → "La.", 改 → "-K".

10. Compressed graphics

Two graphics needed work:

  • the place caption of the first mission, "Tadaga, Esk Republic";
  • the katakana reading under the title logo.

Finding the caption.

  • Graphics are listed in two tables at D000:D484 and D000:E06C. Each starts with a word count, followed by (offset, segment) pairs. The segment's high byte is written to port 0xC2, so the data is read through the 2000 window.
  • The decompressor is at F000:0956. Reading it gave the format, here called "lane RLE":
  • Interleaving by lanes puts the same bitplane bytes next to each other, so runs compress better.

Redrawing it.

  • Claude wrote a decompressor and a compressor in Python and checked that decompress(compress(x)) gives x back.
  • The caption was decompressed, redrawn with the new font and recompressed: 571 bytes against the original 627, so it fits in place. Otherwise the table entry would simply be pointed at free space.

The title subtitle is just 9 sprite tiles, cleared. The last one also holds the edge of the "g" tail in the logo, so that tile is masked rather than wiped.

11. Testing

  • Scripted runs. A Lua script inside headless MAME feeds input sequences like -:300,Start:200,A:60,Right:16,... (button:frames), with one screenshot per step. The steps are then assembled into a contact sheet. A full run from boot to a story scene takes seconds.
  • Memory taps. Lua write and read taps answer "who writes this byte?", which is how the interpreter, the tile ring and the name buffers were found.
  • RAM pokes set up test situations without playing hours: an inventory, a pilot, a stage.
  • The developer menu. The ROM still contains the developers' own menu: stage select and an event checker. Test builds reach it by changing one byte in the title screen's dispatcher, so any mission or cutscene can be checked in seconds. It is never enabled in releases.
  • Savestates taken in the middle of a text box break after a rebuild, because the text moved. Replay from boot or from SRAM saves instead.

12. The double AI review

After v0.9 the whole script went through a second, independent AI. The result was v0.9.1.

The package for ChatGPT:

  • 1,500 rows (1,496 text and 4 graphics) in six CSV batches with the Japanese, the English, the speaker and the row width limits;
  • a 216-entry glossary of names and terms;
  • a long prompt. The prompt asked ChatGPT to research official sources first and write a style guide and a name sheet, then give every row a verdict: CERTAIN, UNCERTAIN, DOUBTFUL or DECIDE (a human must choose).

Claude rechecked every proposal against the Japanese and the window sizes, and I decided the open choices. Some of the fixes:

  • 最適速度 is "optimal speed", not "Cruise" (now "Opt Spd").
  • A dive maneuver boosts every ground attack, not only bombing.
  • A "might be the heroes" line had become a certainty.
  • The maneuver from the proverb 二階から目薬 ("eye drops from the second floor") became "Long Shot".
  • Two names were corrected: Rofe and Gulltoppr.

The reviewer's job is to find suspects; the translator decides with the ROM in hand. Several ChatGPT proposals were right in meaning but didn't fit 18 characters, or relied on names it couldn't verify. Its own report says so.

13. Summary

  • Font first, table second, text third. Once the glyph sheet is decoded, the encoding usually falls out, and kanji can be OCR'd against a system font.
  • Find the script interpreter's control-code table. Unused codes are free hooks: one jump code saves you from repointing thousands of lines.
  • Collect call sites, not just strings. They let you relocate short lines, group lines into pages and translate whole speeches.
  • Model the renderer's limits in the build: widths, rows, tile pools. Don't discover them by playing. If a check can say "this page overflows", playing becomes confirmation instead of search.
  • Keep the build reproducible and defensive. Start from the clean ROM every time, assert original bytes before every patch, recompute the checksum and emit a BPS.
  • Automate the emulator. Scripted input plus screenshots turns "play for an hour to see one window" into a loop of seconds.
  • Where the AI needed a human:
    • names and jokes (taste, not grammar);
    • spotting what looks wrong on a real screen;
    • deciding when "faithful" and "fits in 7 glyphs" conflict.
  • Where it was strong: patient, exhaustive work, such as 2,000 lines, 1,431 pages, every pointer, every width, and reading x86 without getting bored.

The tools: every script described above (extractor, OCR, page engine, font, table and graphics code, BPS encoder, emulator harness) is in BlueWingBlitz_tools.zip: [link]. It contains no ROM and no Japanese script. You run the extractors on your own copy of the game, and the README lists the commands from the clean ROM to an English build. It's research code written for one game, but the ideas carry over.

Sources and tools

  • Japanese text: the WonderSwan Color ROM itself.
  • Names: Square's 2001 announcements as reported by RPGamer (archive.rpgamer.com), the English Wikipedia article "Blue Wing Blitz", the official PlayOnline site (via the Wayback Machine), Weblio and Kotobank for terms and idioms.
  • Aviation terms: the FAA Airplane Flying Handbook and Wikipedia's articles on basic fighter maneuvers.
  • Fonts: Shinonome 6x12 (public domain) for the game; Noto Sans CJK only as the OCR reference.
  • Tools: MAME (headless, Lua scripting), Python, a home-made BPS encoder.

Translation and hacking: made entirely with AI (Claude) for personal use. Please don't upload it to romhacking sites. Questions about the method are very welcome in the comments!

I can post other HIMs for different games upon request.


r/WonderSwan • • 18h ago

RevivalGate - WonderSwan WonderGate Revival Project

36 Upvotes

In 2001 Bandai sold the MobileWonderGate, an adapter accessory for the WonderSwan that connected to a DoCoMo phone so games could post scores, download national rankings, and unlock extras. Bandai shut the service down in 2003, and those features have been unreachable (for the most part) ever since.

RevivalGate is my replacement: new servers for all WonderGate supported WonderSwan games. It's live now for emulators and I hope to eventually support real hardware as well.

What works

27 of the 30 WonderGate games I've tested connect and do something useful. A few highlights:

  • Raku Jongg, GunPey EX, Tare Panda no GunPey, Final Lap 2000, Pocket Fighter, Uchuu Senkan Yamato, and others: Score uploads and national rankings, viewable on the site as well as in the game.
  • Digimon Adventure 02: D1 Tamers and Digimon Tamers: Brave Tamer: The net Grand Prix, ranking battles, and the ladder (Brave Tamer's hall of fame too)
  • Mobile Suit Gundam Vol. 1–3: Game unlocks (the Hammer and Javelin, side stories, and sub-scenarios)
  • SD Gundam G Generation: Gather Beat 1 and 2: The net shop, using the item list and prices players recorded in 2002 (thank you internet)
  • Wonder Classic, Dark Eyes: Battle Gate, Wizardry Scenario 1, Star Hearts, Buffers Evolution, Rainbow Islands, Doraemon, Ring Infinity, Terrors 2, and the MobileWonderGate cart itself (web pages and mail between players)

The full list, with what works per game, is here: https://swanrevival.com/revival-gate/

I have also began translating all of them and have about half done so far. Rest should be done in the next day or so.

How to play

The easiest way is my forked build of MesenCE with the WonderGate adapter built in (Windows, macOS, and Linux). Download it, load a WonderGate game, and open its online menu. There's nothing to configure, other than a completely optional user account which allows you to link your scores from the game.

Other emulators, like Mednafen, can work too. You just have to download the Swan Revival Link app that stands in for the adapter.

How it works

The console never talked to the modem directly. It sent the adapter short commands over the EXT port (open a socket, connect, send, receive, look up a hostname) and the adapter did the networking. So anything that answers those commands can stand in for the adapter and phone. The emulator build (and upcoming adapter) points every old Bandai hostname at our server, which means no ROM or cartridge patching is required. Your games stay exactly as they are.

Nobody recorded the original network traffic that I know of, so each game's server was worked out from the game's own code and tested against the real game running in an emulator.

Real consoles

I'm designing a small USB-C module for the EXT port that replaces the adapter, powered by the console, with your phone carrying the connection. It's still being prototyped, but I'm getting relatively close to finishing it. You can read about it here: https://swanrevival.com/hardware/revival-gate-module/

FAQ

Do I need to patch my ROM or cartridge? No. The games run unmodified. All the work happens in the adapter (emulated, for now) and on the server. I do have most of the games already translated to English though, but that's just a personal preference.

Do I need an account? No. Scores post under the name you enter in the game. If you want them tied to a handle on the site, sign in with Discord or Google and link your emulator with a one-time code. There's no cost and I'm not farming any data here. It's just for fun.

Does it work on a real WonderSwan? Maybe. The USB-C module I'm designing will be the way to do it and I will test it more then. Before then, it should be possible with an ExtFriend-compatible cable and Swan Revival Link on a computer, but that's not actually been tested yet. If you have a cable and want to try, I'd love to hear how it goes.

What about RetroArch, Android, or other emulators? Not yet. Stock MesenCE and ares model the serial port but don't connect it to anything, which is why I added the adapter to MesenCE. The same adapter code could go into a RetroArch core, which would also cover Android. It's something I'll probably eventually look into, but I've just been slammed with the rest of this.

Can you bring back the downloadable content? Only some. Anything the game already contains (rankings, unlocks, shops with item lists I could reconstruct) can come back. Content that only lived on Bandai's servers, like the original downloadable items and maps, is gone unless someone saved it. Where a game needed something from the server, I've filled the gap as faithfully as possible (Gather Beat 1's shop, for example, uses Bandai's final published item list at prices I tried to estimate). If you have old captures, saves made after a download, or magazine scans of WonderGate content, please get in touch.

Which games work? Basically all of them have at least some functionality. The real problem is that a couple of the games really have no data currently, since it was lost when the servers shutdown. I have some ideas for replacements though and they'll be launched shortly. Some games also have features that I'm still trying to fully flesh out, but they are at least partially supported currently.

What happens if Swan Revival goes away? You're not tied to my site. MesenCE's WonderGate settings tab and Swan Revival Link will both accept a different server address.

What does it send from my computer? Only what the game itself sends (its player name, scores, and requests), plus your link token if you linked an account.

Is it free? Yes. There's nothing to pay for. The hardware will cost something when/if it releases, but the service and the emulator are free.

AHA! You're just trying to make money! Yeah right. I'll be lucky to even get close to breaking even, and that's assuming I actually manage to get a working version of some of the hardware. This is just for the love of the game, folks. Love me some WonderSwan.

Do you provide ROMs? Absolutely not. You'll need to dump your own cartridge.

I found a bug or a game behaves oddly. I would almost guarantee you did at this point. Please report it, with the game, what you did, and what it showed. Testing on Windows and Linux is especially welcome. I've done most of my testing on macOS so far. I'm sure I'll wake up to all kinds of reports potentially.

Thanks to trap15 for wonderfence, Keitai Archive for WonderGateway, and the WSdev wiki for its WonderGate protocol notes. This project builds on all three.


r/WonderSwan • • 3h ago

Opened a sealed Crystal Orange Digimon Anode/Cathode Tamer (English) bundle today!

Thumbnail
gallery
29 Upvotes

I thought you guys might appreciate this! I unsealed a Crystal Orange WonderSwan Color bundled with Digimon Anode/Cathode Tamer. The game is in English. Everything is in pristine condition (except for the 2002 AA battery).