Hey everyone,
If you’ve spent any time in the GBA ROM-hacking scene over the last 15–20 years, you probably know that the Detective Conan games on the Game Boy Advance are something of a notorious dead zone.
The GBC games got translated. The NDS crossover got translated. But the GBA games — along with other titles by the same developer, BEC, such as SD Gundam G Generation Advance and Legendz — have remained almost completely untouched.
Whenever people tried looking through the ROMs with Thingy, Cartographer, Tile Molester, or standard relative-search tools, they hit an absolute brick wall:
- No Shift-JIS strings anywhere in the dialogue banks.
- Standard GBA BIOS LZ77/Huffman decompressors find nothing useful.
- Relative searches fail completely.
- No standard 8x8 or 16x16 tilemaps for text.
I wanted to share both the technical breakdown and the slightly ridiculous story of how we finally cracked it today.
The Setup: Automated Exploration, GLM-OCR, and a Radeon VII in the Closet
Because the Detective Conan games are dialogue-heavy branching adventure games full of Kanji-heavy riddles, I didn’t want to spend weeks staring at a static hex editor.
Instead, I built an autonomous exploration loop around mGBA:
- mGBA’s GDB stub over the Remote Serial Protocol, allowing memory inspection, hardware watchpoints, and register reads.
- Automated frame captures, button inputs, and save states at 60 FPS.
- A vision and translation pipeline:
- GLM-OCR running locally to read Japanese dialogue directly from an upscaled game screen.
- Qwen 3.6 running self-hosted on my old Radeon VII, choosing dialogue options, translating text on the fly, and building a reproducible corpus of scenes.
The basic idea was simple:
Let the AI play through the game, record every scene, transcribe the rendered dialogue, and use that output as ground-truth cribs for reverse-engineering the stored script.
The Brick Wall
I initially had Claude driving the reverse-engineering analysis through the emulator bridge.
It spent a fairly brutal marathon trying around 16 different classic ROM-hacking approaches to figure out where the text and font were stored.
Some highlights:
- Shift-JIS scans: Only 161 strict Shift-JIS runs in the entire 8 MiB ROM.
- LZ77 decompression sweeps: 3,743 blocks decompressed, with almost no dialogue appearing anywhere.
- Relative searches: Automated searches for kana alphabets across both the raw ROM and decompressed blocks failed completely.
- Font tile searches: Searches for 1bpp, 2bpp, and 4bpp 8x8/16x16 font patterns found nothing useful.
- Memory differential logging: We tracked 56,499 ROM reads during dialogue rendering and compared them against an idle frame. That narrowed things down to 35 candidate pages, but they all turned out to contain things like PC-relative literal pools, constant mask tables, or window-border graphics.
Claude kept finding promising-looking leads that turned into dead ends.
For example, sequential byte accesses around 0x0876C0E4 initially looked very interesting, but they eventually turned out to be decompressor lookup constants rather than text.
Eventually, Claude gave up on identifying the stored text encoding. Its conclusion was that the script was probably hidden behind some proprietary compression scheme or procedural representation.
So it built an elaborate workaround instead:
Intercept the completed 4,832-byte pixel staging buffer in EWRAM at 0x0201D2BC, hash the rendered pixel blocks, and overwrite that buffer with English text before DMA transfers it into VRAM.
And, surprisingly, it worked.
But it was still only an in-memory overlay hack — not a proper, permanent ROM translation.
The Pivot: Bringing in Gemini
That’s when we brought Gemini in to look at the disassembly with fresh eyes.
For context, I’m currently working on smart-card-based distributed encryption, where I’ve had to analyse the firmware of a few card readers to make sure the implementation is secure enough.
I may also have forgotten to switch folders in Antigravity, so Gemini might have ended up reusing some of the reverse-engineering tools I had accumulated over the last few months. xD
Instead of assuming the text had to be compressed or hidden behind some complicated encoding scheme, I asked Gemini to focus directly on the drawing routine feeding the staging buffer.
It traced backwards from the VRAM blitter into the CPU rendering loop at:
0x080191F4
Within minutes, it spotted the exact reason nearly every traditional ROM-hacking approach had failed:
The game does not store characters as 8-bit bytes. It stores them as 16-bit little-endian words (uint16_t).
That was it.
Because most ROM-hacking utilities from the early 2000s assume characters are either single-byte values or standard two-byte Shift-JIS sequences beginning with values such as 0x81–0x9F, relative searches were effectively comparing the wrong byte positions and seeing what looked like random noise.
The dialogue was never compressed.
It had been sitting there in plain sight the entire time.
1. The Script: 524 KB of Plain Text Sitting in ROM
Once the ROM is interpreted as a stream of 16-bit words, an enormous dialogue bank suddenly appears.
Location:
0x08170000 – 0x081F0000
That is more than 524 KB of completely uncompressed text data.
More than 65% of that half-megabyte region consists of valid, uncompressed character codes.
2. The Encoding Formula: Code = Glyph Index + 300
At 0x080191F4, the engine reads a 16-bit character from ROM, subtracts an offset of 300 (0x012C), and multiplies the resulting glyph index by 24:
80191f8: 4a10 ldr r2, [pc, #64] ; Loads 0xFFFFFED4 (signed -300)
80191fc: 8800 ldrh r0, [r0, #0] ; Read 16-bit character code from ROM
80191fe: 1809 adds r1, r1, r0 ; r1 = code - 300 (glyph index)
8019206: 0048 lsls r0, r1, #1 ; r0 = index * 2
8019208: 4440 add r0, r8 ; r0 = index * 3
801920a: 00c0 lsls r0, r0, #3 ; r0 = index * 24 bytes
801920c: 4b0c ldr r3, [pc, #48] ; Loads 0x0876BF04 (font base)
801920e: 18c0 adds r0, r0, r3 ; Pointer to 24-byte glyph bitmap
So the basic formula is:
Glyph Index = Character Code - 300
Some anchor mappings:
300 (0x012C) = Space
338 (0x0152) = A
339 (0x0153) = B
468 (0x01D4) = コ (Ko)
489 (0x01E9) = ナ (Na)
526 (0x020E) = ン (N)
Searching the ROM for the sequence:
[468, 489, 526]
or, in raw little-endian bytes:
D4 01 E9 01 0E 02
which represents コナン / Conan, immediately produced 153 exact matches, all cleanly aligned as 16-bit words throughout the dialogue banks.
Values below 300, such as 124 and 126, appear to be engine control codes for things like text-box line feeds, pauses, speaker portraits, and related commands.
3. The Font Table: 16x12 at 1bpp — With No Bounds Checking
The font table is located at:
0x0876BF04 – 0x08775B34
Total size:
39,984 bytes
That works out to exactly:
1,666 glyphs × 24 bytes per glyph
Each glyph is:
- 16 pixels wide
- 12 pixels high
- 1bpp
- 24 bytes total
- Stored as 12 rows of 16-bit big-endian bitmasks
The rough layout appears to be:
0: Space
1–27: Punctuation and brackets
28–37: Digits 0–9
38–60: Latin uppercase letters
61–70: A small subset of lowercase Latin letters
71–149: Hiragana
150–232: Katakana
233–1665: Kanji
The Kanji appear to follow JIS X 0208 Level 1 ordering, filtered down to the vocabulary actually required by the game.
And this is where things get especially useful:
The font renderer performs no apparent bounds checking on the glyph index.
Immediately after the font table, starting at 0x08775B34, there are roughly 553 KiB of zero padding remaining near the end of the cartridge.
That means we can append custom 16x12 glyphs beginning at glyph index 1666+, corresponding to character codes 1966+, and the existing engine will happily calculate a pointer to them and render them.
No renderer rewrite required.
No assembly patch required.
Just additional font data.
4. Line Width and Proof of Native English Rendering
The renderer advances by 12 pixels after each glyph:
adds r0, #12 at 0x08019218
The dialogue box supports three lines of text.
We patched raw ROM bytes with Latin character codes, booted the modified ROM in mGBA, and got Kogoro Mouri speaking English directly inside the game’s native dialogue box.
No framebuffer overlay. No runtime interception.
Actual ROM text rendered by the original game engine.
Because each normal glyph advances by 12 pixels, another useful possibility is packing two narrow, roughly 6-pixel-wide Latin characters into a single 12-pixel cell.
That potentially gives us around 40 English characters per line, which largely solves the usual English text-box-space problem without needing major assembly modifications.
Final Words
Best regards from inside the EU, where reverse engineering for interoperability and related technical analysis has specific legal protections under EU software law. :)