Restoring Game Boy Printer functionality on an unmodified Pokémon Gold & Silver Korean cartridge

June 28th, 2026
A screenshot from a Game Boy emulator running Pokémon Gold Korean, showing a glitched black & white screen with hangul writing superimposed on random white squares. A glitched screen we encountered while debugging.

Introduction

Pokémon Gold & Silver is the pair of main games from the second generation of Pokémon video games, released in Japan on November 21st, 1999 and in Europe on April 6th, 2001. A little lesser-known fact is that the games were also released in Korea on April 24th, 2002. Now, I'm not terribly interested in Pokémon games, nor in Korean culture and language. But my friend Siph (https://meemu.org/@Siph) is, and reached out to me to help xem on a very cool endeavour: restoring the Game Boy Printer functionality to Pokémon Gold & Silver Korean version on physical hardware.

All of this was Siph's idea, and I merely brought some of my skills in Game Boy assembly and hacking. But it was so interesting that I thought I'd write a small article about it, because it's probably a world first, and also so that I don't forget anything and have all the info I need on hand.

There is going to be quite a lot of technical stuff regarding Game Boy hardware and assembly, but I'm going to try my best to explain everything to be readable for people with minimal technical background in programming.

Part 0: The Game Boy printer

A Game Boy printer. Photo by Prime Blue: https://commons.wikimedia.org/wiki/File:Game_Boy_Printer.png

The Game Boy printer is a neat little piece of hardware. Not unlike a receipt printer, it connects to a Game Boy using the console's serial interface, also known as the EXT port, in a similar manner to the well-known Game Link.

Mainly used in tandem with the Game Boy camera, to print the photos you'd take with it, it also is compatible with quite a few popular and famous games, such as Austin Powers: Oh, Behave!, VS Lemmings and E.T.: Digital Companion. And also Pokémon Gold & Silver. Specifically, it allows you to print out a screenshot of a Pokédex entry, your party list or your PC boxes. You could then use the printed images as stickers, for example to build yourself a physical Pokédex by sticking them in a notebook, which is honestly quite fun and creative.

However, printing functionality in Pokémon Gold & Silver was removed in the Korean version, probably because the Game Boy Printer was never released for sale in Korea. Even if it was, though, some work on adapting the printing routine to using Hangul https://en.wikipedia.org/Hangul would have had to be performed, because the Korean writing system is implemented quite differently from the English one.

In an unsurprising turn of events, the code for printing stuff is still present in the game's code. Specifically, it is located at the start of bank $21 of the game's ROM, from addresses $21:4000 to $21:4a2a. This means that in theory, one only needs to make the Game Boy run this unused code to print a Pokédex entry. And while it is trivial to do so in an emulator, doing it on real hardware is more complicated; and furthermore, any printed text will come out as garbage, as the code for printing is unchanged from the English version of the game.

Here is the plan:

  1. Test the original printing code in an emulator
  2. Find a way to reach this code on an unmodified version of the physical game
  3. Patch the code to correctly handle Korean text
  4. Physically print a Korean Pokédex entry!

Part 1: Testing on an emulator

The first step is to try and invoke the leftover code on an emulator, in order to experiment and test if it is even possible. I am going to use two different emulators: BGB https://bgb.bircd.org/, a Game Boy emulator with an excellent memory viewer and disassembler, but sadly bereft of a Game Boy Printer implementation; and mGBA https://mgba.io/, an emulator that handles Game Boy Printer perfectly, but with a subpar debugging interface. I am using a ROM that Siph extracted from xyr legally obtained, physical Pokémon Gold Korean copy, and graciously lent me for the duration of this experiment. For BGB, I am also using a symbol file from this wonderful disassembly of the game https://github.com/Narishma-gb/pokegold-kr, which allows me to view function and variable names in the debugger.

First test: directly calling the function from the overworld

Let's start by calling the function directly. On BGB, diverting the execution flow of the Game Boy is as simple as selecting an address in the memory viewer, right-clicking and selecting "Jump to cursor".

The interface of BGB's memory viewer. The interface of BGB's memory viewer.

The function to print a Pokédex entry, named PrintDexEntry in the disassembly of the game, is located at address $442C. However, when navigating there in the memory viewer, I only find a bunch of junk data. This is because of ROM banking.

The Game Boy (and Game Boy Color, they are identical in this aspect) is a 16-bit computer. This means that memory addresses are encoded with 16 bits. In total, the Game Boy can differentiate between 216, or 65,536 different addresses, from addresses $0000 to $FFFF. The system allocates half of this memory space, 32,768 bytes or 32 kibibytes, for the inserted cartridge's ROM. On the other hand, Pokémon Gold & Silver's entire ROM weighs 2 mebibytes; that's 64 times larger than what the console can handle!

Early Game Boy games could fit within 32KiB, but more sophisticated games would need extra space for graphics, level data and more. In order to not be limited by the console's memory space, cartridges have their own little bit of hardware that allows for larger ROM sizes, at the expense of slightly more complicated handling. This system is known as banking, and the piece of hardware that handles that in the cartridge is the MBC (Memory Bank Controller).

The ROM is split in multiple 16KiB chunks, called banks. Two banks can be active at the same time, although for practical reasons, one of them is always bank 0. Bank 0 is thus always mapped to address range $0000 - $3FFF, while another bank is mapped to address range $4000 - $7FFF. Which bank is mapped in this second range is decided through the use of a special hardware register. Writing a byte to address $2000, an otherwise futile operation to perform on read-only memory, is treated by the cartridge as a request to change the mapped bank; the selected bank is the one equal to the value written. For example, writing $21 to $2000 maps bank 33 ($21 in hexadecimal) to memory.

From now on, ROM addresses will be written in the format bank:address. For example, the function I'm trying to use is located at $21:442c.

In my current case, the game is currently running with my player character walking around in the overworld. In this situation, the game uses bank $25, which contains the code for handling NPC scripts, map transitions and player movement. I will need to switch to bank $21 if I want to run the printer code.

In order to switch bank, I will need to first write $21 to $2000, and then call the function. I haven't found an easy way to switch banks in the BGB debugger, so instead I am going to write my own code in RAM and execute that. The code, more specifically, looks like this:

ld a, $21     ; Loads $21 in register A
ld [$2000], a ; Writes A to $2000, switching ROM bank to $21
jp $442c      ; Jumps execution to PrintDexEntry

When assembled and compiled into byte code, it becomes a list of 8 bytes: 3e 21 ea 00 20 c3 2c 44. Note that addresses are encoded in little-endian https://en.wikipedia.org/wiki/Endianness: $442c becomes 2c 44. Using BGB's memory viewer, I write this code in RAM at address $d010, otherwise unused at the moment.

The code is written in memory. The code is written in memory.

I right-click $d010, select "Jump to cursor", and... the game seemingly hangs, a single musical note drawn out to infinity. This was not an unexpected result: not only does BGB automatically stops execution during the VBlank routine, which is not supposed to be jumped out of, but I am also executing the function from the overworld; none of the stuff that the code expects to be present is actually loaded: the pokémon we're trying to print, first and foremost! We need to call the function from where it would originally be called: on the Pokédex screen.

Second test: calling from a Pokédex entry

The Korean Pokédex entry for Arcanine. The Korean Pokédex entry for Arcanine.

From the Pokédex screen, things are a bit easier. Bank $21 is still not loaded, as the code that handles the Pokédex is in bank $10. However, there is a jump table (a list of functions) in this bank called DexEntryScreen_MenuActionJumptable. It encodes the actions you can select at the bottom of the Pokédex entry screen. Even though it only has two active actions (a map of zones where you can catch the Pokémon, and a button to play the Pokémon's cry), there is a third unused listing that performs the necessary operations and setup to call PrintDexEntry.

This unused chunk of code is located at address $10:$428e. If we try to execute it from this screen (after waiting for the VBlank routine to finish), we get a bit closer: the printing music plays, but there's only a black screen. Pressing the A button reveals why the printing function is unused: wildly different text encoding between English and Korean. One of the characters that was supposed to be shown on the screen is interpreted by this version of the game as a control character, and requires pressing the A button to bypass. Afterwards, we are greeted with this wonderful screen:

Game Boy Printer error 2 screen. Game Boy Printer error 2 screen.

This would have worked had BGB emulated the Game Boy printer, but it doesn't. We'll need to switch to mGBA to proceed. After performing the same steps as before, and connecting the virtual printer, we finally get what we were looking for: a printed pokédex entry!

Printed Arcanine entry, full of glitch text. Printed Arcanine entry, full of glitch text.

Part 2: Doing all of this on real hardware

The real challenge happens on physical hardware. With the emulator, we were able to do wild things, like write our own code in RAM, and modify the ROM to change a jump table entry. We do not have that luxury on a physical copy of the game... or at least, half of that luxury.

As it turns out, games from generations 1 and 2 of Pokémon are quite glitchy. The glitches are not game-breaking; it would be extremely unlikely to encounter them during normal gameplay. But if you know where to look, you can achieve ACE: Arbitrary Code Execution. Total control over the memory of the game. Such vulnerabilities are among the most critical in modern software security, as it allows anyone to do anything on a compromised machine. And such a vulnerability is possible in Pokémon Gold & Silver.

Using this comprehensive guide to achieve ACE in the Korean version https://glitchcity.wiki/wiki/Guides:Goldenrod_Wrong_Pocket_TM_ACE_setup_(KOR), we are able to install a RAM writer. This program works similarly to BGB's memory viewer, except that it works from inside the game. However, there is still one critical problem with our current method of reaching printing: we had to modify the ROM. Even with ACE and a memory writer, ROM cannot be modified (it's in the name, after all!).

An in-game memory viewer installed through ACE. An in-game memory viewer installed through ACE.

We will need another way of calling the unused jump table function from inside the Pokédex entry screen. After a lot of thinking and tinkering, we found a way to achieve this: out-of-bounds option selection using OAM hijacking.

How jump tables work

A jump table is simply a list of memory addresses, one after the other. For example, the jump table that interests us is decompiled into this:

DexEntryScreen_MenuActionJumptable:
    dw .Area
    dw .Cry

The dw directive basically tells the assembler to put the value in the output as-is, which in this case are the addresses of each function of the jump table. Then later, in the game's code, we can find something like this:

.do_menu_action:
    ld a, [wDexArrowCursorPosIndex]
    ld hl, DexEntryScreen_MenuActionJumptable
    call Pokedex_LoadPointer
    jp hl

The first instruction fetches a byte from memory, at address wDexArrowCursorPosIndex. This variable contains the current position of the cursor on the option list on the bottom of the Pokédex entry screen. We also load the address of the jump table, and then we call the function Pokedex_LoadPointer. This function essentially performs the following operation, with register A containing the position of the cursor and HL the address of the jump table: HL = Memory[HL + (2 × A)]. We add two times A to the address of the jump table to get the address of the Ath entry in the table; and we read two bytes from that address to get the value it contains, which is another address, this time to the target function.

How a jump table works. How a jump table works.

This works great: with the cursor handled by the rest of the program whenever left or right is pressed, we can select either the map or the cry. But what if? What if the value in A, fetched from that wDexArrowCursorPosIndex variable in memory, was neither 0 nor 1? The function would perform the same: add two times A to HL and read the address there. But where is "there" if we are not reading in the table? The answer is, as always with low-level stuff: whatever is in memory right after the table!

How a jump table can be abused. How a jump table can be abused.

In our case, data right after the jump table happens to be the code for the functions of the jump table. However, the fact that they're function with code to be executed is irrelevant. The jump table's job is to provide two bytes interpreted as an address, and whether those two bytes are from the actual jump table, program code, sprite data or background music notes doesn't matter: bytes are bytes. For example, with a value of $03, the jump table will make the program jump execution to address $cd52. The most-significant byte $cd was the opcode of the second call instruction, while the least-significant byte $52 was part of the address for the first call instruction.

If we are able to manipulate the value of wDexArrowCursorPosIndex, we are able to divert the console's execution to 254 potential addresses. If we are able to find an incidental address in an area of memory that we can manipulate safely without its contents being overwritten, we would be able to write some code at that address that would call the printing function!

However, this is not the only consideration to take. We are modifying the position of the cursor, but that doesn't mean the cursor magically stops existing, or being drawn on screen! The Pokédex code makes the cursor blink, which means multiple writes every second in memory to make it blink. Where the cursor is drawn is determined by the same variable as the one for the jump table, wDexArrowCursorPosIndex, which becomes an index in another table, named DexEntryScreen_ArrowCursorData (at address $21:4244). Each entry in the table is 2 bytes long, and contains an address in memory to which the cursor tile ID is drawn (either $ED representing the right-facing arrow, or $7F representing an empty space). The table has two entries for each of the two possible selectable options at the bottom of the screen. However, an out-of-bounds cursor position means an out-of-bounds table entry, which could end up corrupting important memory locations.

Unlike the jump table containing pointers to functions to jump to, the code that reads from the cursor position table only uses the 7 least-significant bits of the cursor position value. In practice, there are only 128 possible values for the cursor position; cursor position $80 is functionally the same as $00, and will render the cursor on the first option correctly. However, pressing A will still make the program jump to the 129th out-of-bounds entry of the jump table!

The goal is to find a cursor position with a corresponding jump table pointer to writeable, otherwise unused memory, and a cursor position into a memory address to which writing has no bad consequences.

You can find the full list of potential addresses, and where in memory they belong, in here:
Full list of addresses
Value Address Location in memory Cursor graphics address Location in memory
0 424E ROM: DexEntryScreen_MenuActionJumptable.Area C4F8 WRAM: wTilemap
1 427F ROM: DexEntryScreen_MenuActionJumptable.Cry C4FE WRAM: wTilemap
2 01CD ROM: VBlank_Normal.done_oam 424E ROM: SRAM Bank/RTC switch register
3 CD52 WRAM: wAttrmap 427F ROM: SRAM Bank/RTC switch register
4 4AB2 ROM: Pokedex_GetSelectedMon 01CD ROM: SRAM/RTC enable register
5 E4FA Echo RAM, effectively C4FA: wTilemap CD52 WRAM: wAttrmap
6 5FC6 ROM: EvolveAfterBattle_MasterLoop.proceed 4AB2 ROM: SRAM Bank/RTC switch register
7 323E ROM: SkipNames.loop E4FA Echo RAM, effectively C4FA: wTilemap6
8 D0CD WRAM: wTempMonID 5FC6 ROM: SRAM Bank/RTC switch register
9 CD2E WRAM: wAttrmap 323E ROM: ROM bank switch register7
10 5201 ROM: Pokedex_BlackOutBG D0CD WRAM: wTempMonID
11 2ECD ROM: FarCall_hl CD2E WRAM: wAttrMap
12 AF03 SRAM: sMailbox8BackupMessage1 5201 ROM: SRAM Bank/RTC switch register
13 D6E0 WRAM: wPCItems 2ECD ROM: ROM bank switch register7
14 903E VRAM: vTiles2 AF03 SRAM: sMailbox8BackupMessage1
15 D4E0 WRAM: unused D6E0 WRAM: wPCItems
16 C5CD WRAM: unused 903E VRAM: vTiles28
17 CD42 WRAM: wAttrmap D4E0 WRAM: wCmdQueue
18 5231 ROM: Pokedex_LoadSelectedMonTiles C5CD WRAM: unused
19 B9CD SRAM: sBackupPokemonData1 CD42 WRAM: wAttrmap
20 CD34 WRAM: wAttrmap 5231 ROM: SRAM Bank/RTC switch register
21 4AB2 ROM: Pokedex_GetSelectedMon B9CD SRAM: sBackupPokemonData1
22 C0EA WRAM: wChannel5PitchSlideTarget CD34 WRAM: wAttrMap
23 3ED0 ROM: PlayMapMusicBike.play 4AB2 ROM: SRAM Bank/RTC switch register
24 CD04 WRAM: wAttrmap C0EA WRAM: wChannel5PitchSlideTarget
25 5219 ROM: Pokedex_GetSGBLayout 3ED0 ROM: ROM bank switch register7
26 CDC9 WRAM: wAttrmap CD04 WRAM: wAttrMap
27 4AB2 ROM: Pokedex_GetSelectedMon 5219 ROM: SRAM Bank/RTC switch register
28 0EFA ROM: _ClearTilemap CDC9 WRAM: wAttrmap
29 CDD2 WRAM: wAttrmap 4AB2 ROM: SRAM Bank/RTC switch register
30 3A2A ROM: GetCryIndex 0EFA ROM: SRAM/RTC enable register
31 5059 ROM: Pokedex_PutOldModeCursorOAM.CursorOAM CDD2 WRAM: wAttrmap
32 AFCD SRAM: sMailbox10BackupMessage1 3A2A ROM: ROM bank switch register7
33 C93D WRAM: wAnimObject3FramesetID 5059 ROM: SRAM Bank/RTC switch register
34 0BCD ROM: Decompress.donerw AFCD SRAM: sMailbox10BackupMessage1
35 FA52 Echo RAM, effectively DA52: wCurMapObjectEventsPointer C93D WRAM: wAnimObject3FramesetID
36 D003 WRAM: wBattleTransitionSineWaveOffset2 0BCD ROM: SRAM/RTC enable register
37 FAF5 Echo RAM, effectively DAF5: wBackupMapGroup FA52 Echo RAM, effectively DA52: wCurMapObjectEventsPointer
38 D002 WRAM: wBattleTransitionCounter2 D003 WRAM: wBattleTransitionSineWaveOffset2
39 FAF5 Echo RAM, effectively DAF5: wBackupMapGroup9 FAF5 Echo RAM, effectively DAF5: wBackupMapGroup9
40 D001 WRAM: wJumptableIndex D002 WRAM: wBattleTransitionCounter2
41 3EF5 ROM: RestartMapMusic FAF5 Echo RAM, effectively DAF5: wBackupMapGroup
42 2121 ROM: LoadMapTimeOfDay D001 WRAM: wJumptableIndex
43 442C Pokedex_InitUnownMode3 3EF5 ROM: ROM bank switch register7
44 F1CF Echo RAM, effectively D1CF: unused 2121 ROM: ROM bank switch register7
45 01EA ROM: VBlank_Normal.ok2 442C ROM: SRAM Bank/RTC switch register
46 F1D0 Echo RAM, effectively D1D0: unused F1CF Echo RAM, effectively D1CF: wEnemyMonBaseStats
47 02EA ROM: VBlank_Unused 01EA ROM: SRAM/RTC enable register
48 F1D0 Echo RAM, effectively D1D0: unused9 F1D0 Echo RAM, effectively D1D0: unused9
49 03EA ROM: RotatePalettesLeft 02EA ROM: SRAM/RTC enable register
50 CDD0 WRAM: wAttrmap F1D0 Echo RAM, effectively D1D0: wEnemyMonBaseStats
51 34B6 ROM: ClearBGPalettes 03EA ROM: SRAM/RTC enable register
52 36CD ROM: GetTMHMName.copy CDD0 WRAM: wAttrmap
53 CD04 WRAM: wAttrmap 34B6 ROM: ROM bank switch register7
54 52F1 ROM: Pokedex_LoadInvertedFont 36CD ROM: ROM bank switch register7
55 C5CD WRAM: unused CD04 WRAM: wAttrmap
56 CD42 WRAM: wAttrmap 52F1 ROM: SRAM Bank/RTC switch register
57 0458 ROM: EnableLCD C5CD WRAM: unused
58 B9CD SRAM: sBackupPokemonData1 CD42 WRAM: wAttrmap
59 CD34 WRAM: wAttrmap 0458 ROM: SRAM/RTC enable register
60 521D ROM: Pokedex_ApplyUsualPals B9CD SRAM: sBackupPokemonData1
61 CDC9 WRAM: wAttrmap CD34 WRAM: wAttrmap
62 46FE ROM: Pokedex_DrawDexEntryScreenBG 521D ROM: SRAM Bank/RTC switch register
63 B2CD SRAM: sBackupPlayerData31 CDC9 WRAM: wAttrmap
64 3E4A ROM: LowVolume 46FE ROM: SRAM Bank/RTC switch register
65 2111 ROM: LoadMapTimeOfDay B2CD SRAM: sBackupPlayerData31
66 4240 ROM: Pokedex_ReinitDexEntryScreen 3E4A ROM: ROM bank switch register7
67 CDCF WRAM: wAttrmap 2111 ROM: ROM bank switch register7
68 4AA1 ROM: Pokedex_DrawFootprint 4240 ROM: SRAM Bank/RTC switch register
69 AFC9 SRAM: sMailbox10BackupMessage1 CDCF WRAM: wAttrmap
70 D6E0 WRAM: wPCItems 4AA1 ROM: SRAM Bank/RTC switch register
71 A7CD SRAM: sPartyMon6MailAuthor1 AFC9 SRAM: sMailbox10BackupMessage1
72 CD31 WRAM: wAttrmap D6E0 WRAM: wPCItems
73 4774 ROM: Pokedex_DrawOptionScreenBG A7CD SRAM: sPartyMon6MailAuthor1
74 3FCD ROM: TerminateExpBarSound CD31 WRAM: wAttrmap
75 FA51 Echo RAM, effectively DA51: wCurMapObjectEventCount 4774 ROM: SRAM Bank/RTC switch register
76 C6D4 WRAM: wCurDexMode 3FCD ROM: ROM bank switch register7
77 D8EA WRAM: unused FA51 Echo RAM, effectively DA51: wCurMapObjectEventCount
78 CDC6 WRAM: wAttrmap C6D4 WRAM: wCurDexMode
79 4D5C ROM: Pokedex_DisplayModeDescription D8EA WRAM: wEventFlags
80 B9CD SRAM: sBackupPokemonData11 CDC6 WRAM: wAttrmap
81 3E34 ROM: WaitSFX.wait 4D5C ROM: SRAM Bank/RTC switch register
82 CD10 WRAM: wAttrmap B9CD SRAM: sBackupPokemonData1
83 5219 ROM: Pokedex_GetSGBLayout 3E34 ROM: ROM bank switch register7
84 14CD ROM: TrimUnusedHangulChars CD10 WRAM: wAttrmap
85 C941 WRAM: wAnimObject3XCoord 5219 ROM: SRAM Bank/RTC switch register
86 DCFA WRAM: wPokedexCaught 14CD ROM: SRAM/RTC enable register
87 A7C6 SRAM: sPartyMon6MailMessage1 C941 WRAM: wAnimObject3XCoord
88 0520 ROM: InitTimeOfDay DCFA WRAM: wPokedexCaught
89 2B11 ROM: GetMovementPermissions.Right A7C6 SRAM: sPartyMon6MailMessage1
90 1843 ROM: CopyMapObjectStruct 0520 ROM: SRAM/RTC enable register
91 1103 ROM: PlaceEnemysName 2B11 ROM: ROM bank switch register7
92 4333 ROM: Pokedex_UpdateOptionScreen.ArrowCursorData 1843 ROM: SRAM/RTC enable register
93 4ACD ROM: Pokedex_CheckCaught 1103 ROM: SRAM/RTC enable register
94 DC51 WRAM: wPartyMonOTs 4333 ROM: SRAM Bank/RTC switch register
95 4D5C ROM: Pokedex_DisplayModeDescription 4ACD ROM: SRAM Bank/RTC switch register
96 A921 SRAM: sPartyMon5MailBackupMessage1 DC51 WRAM: wPartyMonOTs
97 7EFF ROM: unused 4D5C ROM: SRAM Bank/RTC switch register
98 06E6 ROM: Serial.delay_loop A921 SRAM: sPartyMon5MailBackupMessage1
99 1020 ROM: CheckDict.not_u2 7EFF ROM: Latch clock data register
100 E67E Echo RAM, effectively C67E: unused 06E6 ROM: SRAM/RTC enable register
101 2001 ROM: GetMapSceneID.loop 1020 ROM: SRAM/RTC enable register
102 C901 WRAM: wMysteryGiftMessageCount E67E Echo RAM, effectively C67E: unused
103 D8FA WRAM: wEventFlags 2001 ROM: ROM bank switch register7
104 21C6 ROM: CheckMovingOffEdgeOfMap C901 WRAM: wMysteryGiftMessageCount
105 433D ROM: Pokedex_UpdateOptionScreen.MenuActionJumptable D8FA WRAM: wEventFlags
106 28CD ROM: ScrollMapDown 21C6 ROM: ROM bank switch register7
107 E952 Echo RAM, effectively C952: wAnimObject4 433D ROM: SRAM Bank/RTC switch register
108 01CD ROM: VBlank_Normal.done 28CD ROM: ROM bank switch register7
109 3E52 ROM: FadeOutToMusic E952 Echo RAM, effectively C952: wAnimObject4
110 EA00 Echo RAM, effectively CA00: wBGEffect2BattleTurn2 01CD ROM: SRAM/RTC enable register
111 D001 WRAM: wJumptableIndex 3E52 ROM: ROM bank switch register7
112 C0C9 WRAM: wChannel5 EA00 Echo RAM, effectively CA00: wBGEffect2BattleTurn2
113 F203 Echo RAM, effectively D203: wDayEncounterRate D001 WRAM: wJumptableIndex
114 1AC3 ROM: ScrollingMenuJoypad C0C9 WRAM: wChannel5
115 42C4 ROM: DexEntryScreen_MenuActionJumptable.Print4 F203 Echo RAM, effectively D203: wDayEncounterRate
116 C0C4 WRAM: wChannel4NoteLength 1AC3 ROM: SRAM/RTC enable register
117 F204 Echo RAM, effectively D204: wNiteEncounterRate 42C4 ROM: SRAM Bank/RTC switch register
118 1AC3 ROM: ScrollingMenuJoypad C0C4 WRAM: wChannel4NoteLength
119 42C4 ROM: DexEntryScreen_MenuActionJumptable.Print4 F204 Echo RAM, effectively D204: wNiteEncounterRate
120 6AC4 ROM: PoliwhirlEvosAttacks 1AC3 ROM: SRAM/RTC enable register
121 45C4 ROM: Pokedex_ListingHandleDPadInput 42C4 ROM: SRAM Bank/RTC switch register
122 4943 ROM: UnownModeLetterAndCursorCoords 6AC4 ROM: Latch clock data register
123 4D43 ROM: NewPokedexOrder 45C4 ROM: SRAM Bank/RTC switch register
124 7243 ROM: DragonairEvosAttacks 4943 ROM: SRAM Bank/RTC switch register
125 0643 ROM: Init.wait 4D43 ROM: SRAM Bank/RTC switch register
126 1800 ROM: CheckObjectTime 7243 ROM: Latch clock data register
127 0606 ROM: Init.wait 0643 ROM: SRAM/RTC enable register
128 1801 ROM: CheckObjectTime C4F8 WRAM: wTilemap
129 0602 ROM: Init C4FE WRAM: wTilemap
130 FA02 Echo RAM, effectively DA02: wBoxNames 424E ROM: SRAM Bank/RTC switch register
131 C6D4 WRAM: wCurDexMode 427F ROM: SRAM Bank/RTC switch register
132 28B8 ROM: ScrollMapUp 01CD ROM: SRAM/RTC enable register
133 7814 ROM: PiloswineEvosAttacks CD52 WRAM: wAttrmap
134 D4EA WRAM: unused 4AB2 ROM: SRAM Bank/RTC switch register
135 CDC6 WRAM: wAttrmap E4FA Echo RAM, effectively C4FA: wTilemap6
136 4ADD ROM: Pokedex_OrderMonsByMode 5FC6 ROM: SRAM Bank/RTC switch register
137 26CD ROM: FillMapConnections 323E ROM: ROM bank switch register7
138 AF4E SRAM: sMailbox9BackupMessage1 D0CD WRAM: wTempMonID
139 D0EA WRAM: wTempMonMaxHP CD2E WRAM: wAttrMap
140 EAC6 Echo RAM, effectively CAC6: unused 5201 ROM: SRAM Bank/RTC switch register
141 C6D1 WRAM: wDexListingCursor 2ECD ROM: ROM bank switch register7
142 97CD VRAM: vTiles2 AF03 SRAM: sMailbox8BackupMessage1
143 CD40 WRAM: wAttrmap D6E0 WRAM: wPCItems
144 5201 ROM: Pokedex_BlackOutBG 903E VRAM: vTiles28
145 003E ROM: WaitOneLine.loop D4E0 WRAM: wCmdQueue
146 01EA ROM: VBlank_Normal.ok2 C5CD WRAM: unused
147 C9D0 WRAM: wAnimObject9TileID CD42 WRAM: wAttrmap
148 01CD ROM: VBlank_Normal.done_oam 5231 ROM: SRAM Bank/RTC switch register
149 3E52 ROM: FadeOutToMusic B9CD SRAM: sBackupPokemonData1
150 EA0B Echo RAM, effectively CA0B: wBGEffect5JumptableIndex CD34 WRAM: wAttrMap
151 D001 WRAM: wJumptableIndex 4AB2 ROM: SRAM Bank/RTC switch register
152 AFC9 SRAM: sMailbox10BackupMessage1 C0EA WRAM: wChannel5PitchSlideTarget
153 D6E0 WRAM: unused 3ED0 ROM: ROM bank switch register7
154 A7CD SRAM: sPartyMon6MailAuthor1 CD04 WRAM: wAttrMap
155 CD31 WRAM: wAttrmap 5219 ROM: SRAM Bank/RTC switch register
156 47EB ROM: Pokedex_DrawSearchScreenBG CDC9 WRAM: wAttrmap
157 3FCD ROM: TerminateExpBarSound 4AB2 ROM: SRAM Bank/RTC switch register
158 3E51 ROM: FadeOutToMusic 0EFA ROM: SRAM/RTC enable register
159 EA01 Echo RAM, effectively CA01: wBGEffect2Param CDD2 WRAM: wAttrmap
160 C6D5 WRAM: wDexSearchMonType1 3A2A ROM: ROM bank switch register7
161 EAAF Echo RAM, effectively CAAF: unused 5059 ROM: SRAM Bank/RTC switch register
162 C6D6 WRAM: wDexSearchMonType2 AFCD SRAM: sMailbox10BackupMessage1
163 BDCD SRAM: unused1 C93D WRAM: wAnimObject3FramesetID
164 AF4E SRAM: sMailbox9BackupMessage1 0BCD ROM: SRAM/RTC enable register
165 DBEA WRAM: wPartyMon5Moves FA52 Echo RAM, effectively DA52: wCurMapObjectEventsPointer
166 3EC6 ROM: PlayMapMusicBike.play D003 WRAM: wBattleTransitionSineWaveOffset2
167 2111 ROM: LoadMapTimeOfDay FAF5 Echo RAM, effectively DAF5: wBackupMapGroup9
168 41FA ROM: Pokedex_UpdateDexEntryScreen.return_to_prev_screen D002 WRAM: wBattleTransitionCounter2
169 CDCF WRAM: wAttrmap FAF5 Echo RAM, effectively DAF5: wBackupMapGroup
170 34B9 ROM: WaitBGMap D001 WRAM: wJumptableIndex
171 103E ROM: CheckDict.not_diacritic 3EF5 ROM: ROM bank switch register7
172 19CD ROM: UpdateSprites 2121 ROM: ROM bank switch register7
173 CD52 WRAM: wAttrmap 442C ROM: SRAM Bank/RTC switch register
174 4114 ROM: Pokedex_IncrementDexPointer F1CF Echo RAM, effectively D1CF: wEnemyMonBaseStats
175 11C9 ROM: _ContText.communication 01EA ROM: SRAM/RTC enable register
176 43D6 ROM: Pokedex_UpdateSearchScreen.ArrowCursorData F1D0 Echo RAM, effectively D1D0: unused
177 4ACD ROM: Pokedex_CheckCaught 02EA ROM: SRAM/RTC enable register
178 CD51 WRAM: wAttrmap F1D0 Echo RAM, effectively D1D0: wEnemyMonBaseStats
179 4E64 ROM: Pokedex_UpdateSearchMonType 03EA ROM: SRAM/RTC enable register
180 BDDC SRAM: unused1 CDD0 WRAM: wAttrmap
181 214E ROM: LoadMapTimeOfDay.PushAttrmap 34B6 ROM: ROM bank switch register7
182 FFA9 HRAM: hJoyPressed 36CD ROM: ROM bank switch register7
183 E67E Echo RAM, effectively C67E: unused CD04 WRAM: wAttrmap
184 200A ROM: GetMapSceneID.loop 52F1 ROM: SRAM Bank/RTC switch register
185 7E10 ROM: unused C5CD WRAM: unused
186 01E6 ROM: VBlank_Normal.ok2 CD42 WRAM: wAttrmap
187 0120 ROM: VBlank_Cutscene 0458 ROM: SRAM/RTC enable register
188 FAC9 Echo RAM, effectively DAC9: wPhoneList B9CD SRAM: sBackupPokemonData1
189 C6D8 WRAM: wDexArrowCursorPosIndex5 CD34 WRAM: wAttrmap
190 E021 Echo RAM, effectively C021: wChannel1VibratoRate 521D ROM: SRAM Bank/RTC switch register
191 CD43 WRAM: wAttrmap CDC9 WRAM: wAttrmap
192 5228 ROM: Pokedex_LoadPointer 46FE ROM: SRAM Bank/RTC switch register
193 CDE9 WRAM: wAttrmap B2CD SRAM: sBackupPlayerData31
194 5201 ROM: Pokedex_BlackOutBG 3E4A ROM: ROM bank switch register7
195 003E ROM: WaitOneLine.loop 2111 ROM: ROM bank switch register7
196 01EA ROM: VBlank_Normal.ok2 4240 ROM: SRAM Bank/RTC switch register
197 C9D0 WRAM: wAnimObject9TileID CDCF WRAM: wAttrmap
198 04C0 ROM: FixDays.modl 4AA1 ROM: SRAM Bank/RTC switch register
199 C3F2 WRAM: wTilemap AFC9 SRAM: sMailbox10BackupMessage1
200 C41A WRAM: wTilemap D6E0 WRAM: wPCItems
201 C4A6 WRAM: wTilemap A7CD SRAM: sPartyMon6MailAuthor1
202 C4CE WRAM: wTilemap CD31 WRAM: wAttrmap
203 43E8 ROM: Pokedex_UpdateSearchScreen.MenuAction_MonSearchType 4774 ROM: SRAM Bank/RTC switch register
204 43E8 ROM: Pokedex_UpdateSearchScreen.MenuAction_MonSearchType 3FCD ROM: ROM bank switch register7
205 43EF ROM: Pokedex_UpdateSearchScreen.MenuAction_BeginSearch FA51 Echo RAM, effectively DA51: wCurMapObjectEventCount
206 4439 ROM: Pokedex_UpdateSearchScreen.MenuAction_Cancel C6D4 WRAM: wCurDexMode
207 99CD VRAM: vBGMap0 D8EA WRAM: wEventFlags
208 CD4E WRAM: wAttrmap CDC6 WRAM: wAttrmap
209 4EBD ROM: Pokedex_PlaceSearchScreenTypeStrings 4D5C ROM: SRAM Bank/RTC switch register
210 CDC9 WRAM: wAttrmap B9CD SRAM: sBackupPokemonData1
211 4F75 ROM: Pokedex_SearchForMons 3E34 ROM: ROM bank switch register7
212 113E ROM: TMCharText CD10 WRAM: wAttrmap
213 C221 WRAM: wBGPals1 5219 ROM: SRAM Bank/RTC switch register
214 CF41 WRAM: unused 14CD ROM: SRAM/RTC enable register
215 D7FA WRAM: unused C941 WRAM: wAnimObject3XCoord
216 A7C6 SRAM: sPartyMon6MailMessage1 DCFA WRAM: wPokedexCaught
217 1620 ROM: Serve1bppRequest.next A7C6 SRAM: sPartyMon6MailMessage1
218 DDCD WRAM: wEggMonExp 0520 ROM: SRAM/RTC enable register
219 CD4A WRAM: wAttrmap 2B11 ROM: ROM bank switch register7
220 4FF6 ROM: Pokedex_DisplayTypeNotFoundMessage 1843 ROM: SRAM/RTC enable register
221 E0AF Echo RAM, effectively C0AF: wChannel4LoopCount 1103 ROM: SRAM/RTC enable register
222 CDD6 WRAM: wAttrmap 4333 ROM: SRAM Bank/RTC switch register
223 47EB ROM: Pokedex_DrawSearchScreenBG 4ACD ROM: SRAM Bank/RTC switch register
224 3FCD ROM: TerminateExpBarSound DC51 WRAM: wPartyMonOTs
225 CD51 WRAM: wAttrmap 4D5C ROM: SRAM Bank/RTC switch register
226 4EBD ROM: Pokedex_PlaceSearchScreenTypeStrings A921 SRAM: sPartyMon5MailBackupMessage1
227 B9CD SRAM: sBackupPokemonData1 7EFF ROM: Latch clock data register
228 C934 WRAM: unused 06E6 ROM: SRAM/RTC enable register
229 D2EA WRAM: wObject1MapX 1020 ROM: SRAM/RTC enable register
230 FAC6 Echo RAM, effectively DAC6: wSafariTimeRemaining E67E Echo RAM, effectively C67E: unused
231 C6D0 WRAM: wDexListingScrollOffset2 2001 ROM: ROM bank switch register7
232 E0EA Echo RAM, effectively C0EA: wChannel5PitchSlideTarget C901 WRAM: wMysteryGiftMessageCount
233 FAC6 Echo RAM, effectively DAC6: wSafariTimeRemaining D8FA WRAM: wEventFlags
234 C6D1 WRAM: wDexListingCursor 21C6 ROM: ROM bank switch register7
235 E1EA Echo RAM, effectively C1EA: wLinkReceivedSyncBuffer 433D ROM: SRAM Bank/RTC switch register
236 FAC6 Echo RAM, effectively DAC6: wSafariTimeRemaining 28CD ROM: ROM bank switch register7
237 C1D5 WRAM: wPrevDexEntry E952 Echo RAM, effectively C952: wAnimObject4
238 03EA ROM: RotatePalettesLeft 01CD ROM: SRAM/RTC enable register
239 AFD0 SRAM: sMailbox10BackupMessage1 3E52 ROM: ROM bank switch register7
240 D0EA WRAM: wTempMonMaxHP EA00 Echo RAM, effectively CA00: wBGEffect2BattleTurn2
241 EAC6 Echo RAM, effectively CAC6: unused D001 WRAM: wJumptableIndex
242 C6D1 WRAM: wDexListingCursor C0C9 WRAM: wChannel5
243 01CD ROM: VBlank_Normal.done_oam F203 Echo RAM, effectively D203: wDayEncounterRate
244 3E52 ROM: FadeOutToMusic 1AC3 ROM: SRAM/RTC enable register
245 EA09 Echo RAM, effectively CA09: wBGEffect4Param 42C4 ROM: SRAM Bank/RTC switch register
246 D001 WRAM: wJumptableIndex C0C4 WRAM: wChannel4NoteLength
247 CDC9 WRAM: wAttrmap F204 Echo RAM, effectively D204: wNiteEncounterRate
248 5201 ROM: Pokedex_BlackOutBG 1AC3 ROM: SRAM/RTC enable register
249 003E ROM: WaitOneLine.loop 42C4 ROM: SRAM Bank/RTC switch register
250 01EA ROM: VBlank_Normal.ok2 6AC4 ROM: Latch clock data register
251 C9D0 WRAM: wAnimObject9TileID 45C4 ROM: SRAM Bank/RTC switch register
252 E0AF Echo RAM, effectively C0AF: wChannel4LoopCount 4943 ROM: SRAM Bank/RTC switch register
253 CDD6 WRAM: wAttrmap 4D43 ROM: SRAM Bank/RTC switch register
254 4869 ROM: Pokedex_DrawSearchResultsScreenBG 7243 ROM: Latch clock data register
255 043E ROM: DisableLCD 0643 ROM: SRAM/RTC enable register

1 Since SRAM is closed at the moment of the jump, all reads return $FF, in turn jumping in the middle of ROM code through reset vector 7; and writes do nothing.

2 This address corresponds to multiple different variables, sharing the same location in memory and trusting none of them are active at the same time as another. Only one variable name is shown here.

3 By pure coincidence, this address corresponds exactly to the start of function PrintDexEntry! Unfortunately, that function is in ROM bank $21, and at the time of pressing A on the Pokédex screen, we are switched into ROM bank $10.

4 Unfortunately, this falls directly at the end of the function, immediately executing a ret and finishing handling the A button press.

5 Amusingly, this is the address of the very variable we use here to perform an out-of-bounds jump. If written to by the cursor blinking, this would change the cursor's position to $ED (237) or $7F (127) depending on the cursor's blinking state.

6 This means the cursor will be actually drawn on screen, somewhere unintended.

7 This instantly switches the current active bank to either 7F or 6D (depending on the cursor being on or off in its blinking state), which instantly crashes the game.

8 Since this writes into VRAM, graphics on the screen will be modified on the fly. What is modified depends on the area of VRAM being modified, but it could be either tiles or background map.

9 Interestingly, the memory address modified by the cursor blinking is the exact same as the jump address. This would force the first instruction executed to be either $7F (ld a, a) or $ED (invalid opcode).

There are many possible cursor positions to choose from; the first one down the list is position $08 (the index for the ninth option). It jumps execution to $d0cd, an area of RAM only used during Pokémon battles, and its cursor graphics position is $5fc6, the cartridge register that changes what is mapped in the memory range $a000 - $bfff. Interacting with that register does nothing for us, since neither SRAM nor the real-time clock is of use for our exploit.

We now have our target, but we still need a way to actually force the cursor position to this out-of-bounds value. You might think that writing the value at the memory address for the variable before opening the Pokédex might work, but unfortunately the value is reset to 0 whenever you open an entry. We need a way to modify the variable in memory from within the Pokédex. This is where OAM hijack comes into play.

The High RAM OAM DMA routine

High RAM (often abbreviated to HRAM) is a tiny region of memory mapped to addresses $ff80 to $fffe. It is a completely normal RAM area, being readable and writeable at any time, with a capacity of 127 bytes. However, it has two special characteristics that makes its use very specific:

  • The Game Boy CPU has special instructions that allows reading and writing to HRAM faster, taking one less byte than similar instructions targeting areas outside of HRAM.
  • It is the only area of memory that remains accessible during a DMA transfer on the original Game Boy (on Game Boy Color, the cartridge and work RAM are on separate buses, so either is still accessible when the other is used in the DMA transfer).

That last point is the reason for the code we're going to look at. DMA (Direct Memory Access) is a procedure that allows the system to copy a large chunk of data from one area of memory to another in a very efficient manner, compared to a regular program that would do the same thing. This is used by many games, including Pokémon Gold & Silver, to transfer data from ROM or WRAM to OAM (Object Attribute Memory), a special area of memory used by the system's graphics processor to draw sprites on screen.

When performing a DMA transfer, large chunks of memory become inaccessible, requiring the code being executed by the CPU to be located in an area of memory exempt from this lockdown; HRAM is the only one. Consequently, we can find a function at address $ff80 called hTransferShadowOAM, copied from ROM by the game when it boots. The part that interests us is when this function is called: the game performs an OAM DMA transfer every frame!

This routine is copied from ROM once at startup and is never modified afterwards. We can then modify it using our ACE memory writer to have instructions of our choice executed every frame.

Unless proper care is taken to preserve the actual functionality of the OAM DMA routine, modifying it will stop sprites from being displayed on screen. This is only a visual effect, and the game works fine (although that makes playing it harder!).

The normal routine looks like this:

hTransferShadowOAM:
    ld a, HIGH(wShadowOAM)
    ldh [rDMA], a
    ld a, OAM_COUNT
.wait:
    dec a
    jr nz, .wait
    ret

It initiates the DMA transfer, then waits for a certain amount of time before returning. We can simply replace the ret instruction with some of our code, to be executed after the transfer:

ld a, $08
ld [$c6d8], a
ret

This code load the value $08 in register A, then writes that value to $c6d8, the address of wDexArrowCursorPosIndex in WRAM, then returns. The original OAM DMA transfer is preserved and this modification shouldn't change how the game behaves.

Writing our payload

With all of this active, opening a Pokédex entry and pressing A will make the CPU jump to address $d0cd, but there isn't anything there yet. Since we can write whatever we want in memory, let's write a jump instruction to our unused printing routine:

jp $428e

No need for anything more complicated yet.

Putting it all together

With this, we have our plan:

  1. Write the jump instruction to $428e at address $d0cd
  2. Overwrite the OAM DMA transfer function at $ff80 to write $08 at address $c6d8
  3. Navigate to a Pokédex entry, ignoring that everyone in town has become invisible
  4. Press A

Here is the detailled procedure, step by step, to write each byte at the correct address:

  1. Write the following bytes starting at address $d0cd:
    • $d0cd: $c3
    • $d0ce: $8e
    • $d0cf: $42
  2. Write the following bytes, starting at $ff8a:
    • $ff8a: $08
    • $ff8b: $ea
    • $ff8c: $d8
    • $ff8d: $c6
    • $ff8e: $c9
  3. Write $3e at $ff89
The reason for first writing the rest of the bytes before writing $3e at $ff89 is because the original OAM DMA transfer is performed every frame. On physical hardware, with a typical RAM writer, we can't modify multiple values in memory at once. If we started writing our modified instructions starting by overwriting the original ret, the partially-modified routine would still continue to be run every frame, and it would probably crash the game by executing beyond the function into the rest of memory. This is why we write our procedure beyond the original ret instruction ($c9), allowing us to modify the rest of the bytes freely, before writing the first byte of the routine and allowing it to continue past its original stopping point.

Once this is done, we can navigate to a Pokédex entry, and press A; if the Game Boy is connected to a printer, it will print the entry successfully! We did it!

Printed entry on real hardware. Photo by Siph. Printed entry on real hardware. Photo by Siph.

Part 3: Patching code without modifying the ROM

The patch that restores printing for the Korean version for Pokémon Gold & Silver was written by Narishma. You can find a modified version of the disassembly, that includes the patched code, on Narishma's GitHub repository https://github.com/Narishma-gb/pokegold-kr/tree/gb-printer.

Now that we proved we can print something from the hardware version of the game, we need to find a way to actually print text, and not glitched garbage. We have access to a patched version of the ROM, that would restore the functionality properly by adding a menu entry and fixing Hangul printing, but as we said before, modifying the ROM isn't an option. We need to do this without any external modifications, simply using tools provided to us by the game itself. Ideally, we would be able to make our modification permanent, so that we can print a Pokédex entry even after resetting the game.

Our only solution is to write the patched code somewhere in memory, and call that code instead of the original one present in the ROM. But we need a place in memory that is both large enough to host such a large chunk of code, and is also preserved between resets. This is where we can make good use of SRAM.

SRAM

SRAM, or Save RAM, is a chunk of memory mapped at addresses $A000 to $BFFF. This memory is special: it does not reside on the Game Boy system itself; it is actually part of the game cartridge. It is also able to retain information even when the cartridge is not plugged in, using an on-board battery. The size of SRAM depends on the game, and for Pokémon Gold & Silver, SRAM is 32 KiB, divided into four 8 KiB banks. It typically contains your save file data. In order to be accessed, SRAM needs to be "open". This is done by writing a specific byte at an address in ROM, which is interpreted by the cartridge as a request to open SRAM. A similar operation is performed to close it. Trying to access SRAM before opening it will result in garbage data read, and writes do nothing.

Not all of SRAM is used to store your save, though. Several areas of SRAM as unused, or are used for other purposes. Famously, $00:$a000 to $00:$a600 is used as scratch (temporary) memory to store decompressed Pokémon sprite data.

I highly recommend this excellent video https://www.youtube.com/watch?v=aF1Yw_wu2cM by Retro Game Mechanics Explained, which touches on how Pokémon sprite data is handled. The video covers Gen I, but it shouldn't differ too much from Gold & Silver.

We need to find an area of SRAM that would be able to contain the entirety of our patched code, while also not overwriting important part of our game's save. We estimated at first that our patched code would take around 1000 bytes. We first tried $00:$a600, which holds the mail held by Pokémon in our party; but we realised that while the overall area is large enough, parts of it are overwritten by unrelated chunks of our patched code when saving. That was due to "backup" data: when saving, the game saves duplicate data for certain things, like mail, and so parts of our code that resided in this backup area was overwritten by earlier parts of the code.

We settled on $0:$a9b5: it holds data for mail in the player's mailbox, which we felt isn't particularly important data to preserve. Furthermore, the entirety of the mail box is 791 bytes long, which we felt would be close enough to the estimated 1000 bytes needed, that we wouldn't need to shave too much to fit it in.

Patching only what we need

Blindly copying the entirety of all functions that handle printing would necessitate thousands of bytes of space, which we do not have. We need to be strategic with our patched code, and only include what needs to be included. Going through the disassembly of the game, I tracked which functions were modified and included them in my patch, leaving the rest of the function out of the patch.

  • If a function's code is modified to make printing possible, it is copied in the patch and all references to this function are modified to point to the one in our patch
  • If a function calls another function that is included in our patch, then copy it to our patch itself.
  • Otherwise, leave this function out of the patch, and we will use it as-is from the game's ROM

In addition to this, we had to make several modifications to the patch itself:

Prologue and epilogue in PrintDexEntry

Originally, printing was achieved through the jump table we mentionned before. It did not just call the relevant function in bank $21, but it also performed several operations to set up printing:

call Pokedex_ApplyPrintPals
ld a, [wPrevDexEntryBackup]
push af
ld a, [wPrevDexEntryJumptableIndex]
push af
ld a, [wJumptableIndex]
push af

It also does stuff after printing, to restore normal Pokédex functionality:

pop af
ld [wJumptableIndex], a
pop af
ld [wPrevDexEntryJumptableIndex], a
pop af
ld [wPrevDexEntryBackup], a
call ClearBGPalettes
call DisableLCD
call Pokedex_LoadInvertedFont
call Pokedex_RedisplayDexEntry
call EnableLCD
call WaitBGMap
call Pokedex_ApplyUsualPals
ret

To ensure the game runs properly after printing something, we need to include these two snippets, at the start and at the end of the main printing function respectively.

We also need to do two bank switches: right after the prologue, we need to switch to ROM bank $21 (in order to have access to unmodified printing code); and right before the epilogue, switch to ROM bank $10 (to access functions related to the Pokédex). This emulates the farcall performed by the original code.

Prevent SRAM closing

The original printing routine actually makes use of SRAM: it uses the scratch memory area normally used for Pokémon sprite decompression. Because of this, there are several instances in the code that opens and closes SRAM. Since the code we want to run resides in SRAM, we do not want it to close while running code from it (otherwise we will lose access to our code and inevitably crash the console). Similarly, opening SRAM does nothing, since it needs to be already opened to reach this point. Thus, all requests to open or close SRAM were removed from the code.

RAM entry point modifications

Since we are now executing code from SRAM, we need to modifying our payload we inject into memory at $d0cd. Before calling the entry function, we need to open SRAM manually, and close it afterwards. Our new function at $d0cd looks like this:

xor a
call OpenSRAM
call $a9b5
call CloseSRAM
ret

When compiled, it becomes af cd 7a 31 cd b5 a9 cd 94 31 c9, to be written at $d0cd.

Final patch file

With this set of rules, we produced a patch file ./poke_gold_silver_korean_printer_patch.asm that only contains necessary modified functions and data. Using an appropriate symbols file, it can be compiled with QuickRGBDS https://github.com/M4n0zz/QuickRGBDS down to 524 raw bytes, adapted for the specific entry point of $a9b5.

You can find the compiled code here:
Compiled byte code patch
cd 0b 52 fa 03 d0 f5 fa 02 d0  
f5 fa 01 d0 f5 3e 21 d7 21 00  
88 11 00 40 01 80 36 cd 40 0e  
af e0 ae cd 48 47 f0 ff f5 af  
e0 0f 3e 09 e0 ff cd 00 40 3e  
13 ea fa ca 21 a0 c3 11 90 c9  
01 54 01 cd c2 31 21 e4 ca 01  
14 00 3e 32 cd f4 31 21 e0 cc  
11 00 a0 01 54 01 cd c2 31 21  
54 a1 01 14 00 af cd f4 31 cd  
e6 0e 3e e4 cd 54 0c cd 2e 03  
21 a0 ff 7e f5 36 04 cd a0 ab  
f1 e0 a0 cd 11 44 af e0 0f f1  
e0 ff cd 25 44 0e 08 cd 46 3e  
cd 2e 03 0d 20 f7 3e 10 cd 10  
00 f1 ea 01 d0 f1 ea 02 d0 f1  
ea 03 d0 cd b6 34 cd 36 04 cd  
f1 52 cd c5 42 cd 58 04 cd b9  
34 cd 1d 52 c9 f5 cd 00 40 f1  
ea fa ca cd 24 47 c9 fa 01 d0  
5f 16 00 21 89 aa 19 19 2a 66  
6f e9 b1 aa 43 41 20 41 73 ab  
80 41 2e 41 c5 40 80 41 20 41  
de 40 80 41 20 41 a1 41 63 40  
6d 40 20 41 d2 aa 71 40 b0 41  
b3 41 cd fb 41 21 b7 42 cd e2  
41 af ea 8e c9 ea 8f c9 3e 09  
ea 81 c9 cd 59 40 cd c3 41 3e  
01 ea f8 ca c9 cd fb 41 21 b7  
42 cd e2 41 af ea 8e c9 ea 8f  
c9 3e 09 ea 81 c9 cd 59 40 cd  
c3 41 c9 fa 81 c9 ee ff c6 0a  
21 00 00 11 28 00 a7 28 04 19  
3d 18 f9 5d 54 21 00 a0 19 7d  
ea 0b cb 7c ea 0c cb 21 90 c9  
19 5d 54 21 00 c7 06 00 0e 28  
1a 13 c5 d5 e5 cb 37 57 e6 f0  
5f 7a e6 0f 57 e6 08 7a 20 04  
f6 90 18 02 f6 80 57 e5 21 0b  
cb 2a 66 80 6f 30 01 24 cb 5e  
e1 28 19 f3 3e 01 e0 4f 06 10  
f0 41 e6 02 20 fa 1a 13 22 05  
20 f4 af e0 4f fb 18 06 01 01  
21 cd fc 0d e1 11 10 00 19 d1  
c1 04 0d 20 ab c9 cd fb 41 21  
81 c9 7e a7 ca c5 40 21 c3 42  
cd e2 41 cd ee aa 3e 80 ea 8e  
c9 3e 02 ea 8f c9 cd 19 42 cd  
59 40 cd c3 41 3e 02 ea f8 ca  
c9 cd fb 09 cd f2 46 38 17 fa  
01 d0 cb 7f 20 0e cd 7a aa cd  
53 47 cd 81 47 cd 2e 03 18 e3  
a7 c9 37 c9
Diagram: How the entire thing works. How the entire thing works.

Part 4: Physically print a correct Korean Pokédex entry!

The complete procedure is now as follows:

  1. Attain ACE on a Pokémon Gold or Silver Korean cartridge https://glitchcity.wiki/wiki/Guides:Goldenrod_Wrong_Pocket_TM_ACE_setup_(KOR) and obtain the Pokédex.
  2. Through a RAM writer or any other mean of writing data to memory, write the 524 bytes representing the patch that allows the game to print Hangul correctly, starting at address $00:$a9b5 in SRAM. Note that this overwrites your mailbox data.
  3. Save the game to commit the patch to the save file.
  4. Using a RAM writer, write the following bytes to memory, starting at $d0cd:
    • $d0cd: $af
    • $d0ce: $cd
    • $d0cf: $7a
    • $d0d0: $31
    • $d0d1: $cd
    • $d0d2: $b5
    • $d0d3: $a9
    • $d0d4: $cd
    • $d0d5: $94
    • $d0d6: $31
    • $d0d7: $c9
  5. Write the following bytes in memory, starting at $ff8a:
    • $ff8a: $08
    • $ff8b: $ea
    • $ff8c: $d8
    • $ff8d: $c6
    • $ff8e: $c9
  6. Write $3e at $ff89.
  7. Open a Pokédex entry, and make sure a Game Boy Printer is plugged in and turned on.
  8. Press the A button to print.
A photo of the final printed entries. It works! Photo by Siph. A photo of the final printed entries. It works! Photo by Siph.

And it works! Unless we are both mistaken, Siph and I believe this is the first time that someone has ever printed a Pokédex entry (or anything, really) from an unmodified Pokémon Gold & Silver Korean game. All of this would not have been possible without the amazing work of Narishma, who not only decompiled the game, but also wrote the patched code that allows correct Hangul printing. We merely cherry-picked the code and crammed it in our save file to run it on the original ROM.

Thank you for reading! Coming soon (hopefully), an edit to this article linking to Siph's own writeup for this little project.