Pokémon Crystal any% speedrun route
The current Pokémon Crystal any% glitched speedrun route is based on 0x1500 control code arbitrary code execution. It triggers the glitch by using the bad clone glitch to get a Pokémon with an unterminated nickname, and viewing its name in the PC withdraw screen. It also makes use of two luck manipulations to get desirable values for the Trainer ID, the Lucky ID, and DVs of the starter. This page documents the route and analyzes the glitches used in detail.
The content of this page is based on the advanced route guide by entrpntr. Details unrelated to glitches/manipulations (i.e. for time saving only) are omitted.
- Clear saves between resets.
- This is required any time you start a New Game, since your lucky ID persists regardless of having a save file.
- Manipulate for a Trainer ID of 0x26FB (09979) and Lucky ID of 0x186F (06255).
- Manipulate for a Cyndaquil with DVs E9xx (see the original route guide for detailed inputs).
- This manipulation needs the player name to be 3 characters (the preset name "MAT" will do).
- Swap Leer and Tackle during the Rival fight to set up the move order.
- Win the rival fight to level up and learn Smokescreen.
- The moves of Cyndaquil should be Leer, Tackle, Smokescreen in this order (Tackle, Smokescreen, Leer also works, but takes more time).
- Catch any encounter (2 Pokémon are required for cloning).
- Run from all other encounters (guarantees safe experience values).
- The experience and stat experience values after defeating the rival do affect the ACE. They are not a crucial part of it (if they were "safe" values, the box names can be changed to account for that), but this particular route needs those values.
Bad clone glitch
- Go to the PC on the 2nd floor of Cherrygrove Pokémon Center.
- Rename boxes:
- BOX 1: T 0 'v é N 5 'v 's
- BOX 2: é n 6 'v f é R 5
- BOX 3: f é o 6 p é é 6
- BOX 4: é 5 5 PK PK 'd
- Note: Characters in italic don't matter (they are overwritten by the ACE).
- Move Cyndaquil to the second slot in party, and save the game ("move PkMn w/o mail" is the fastest way to achieve both).
- Do the bad clone glitch by depositing one of the Pokémon, switching boxes and hard resetting during the save.
- Either a "real" bad clone (level 0) or a "pseudo" bad clone (correct level, but unterminated nickname; referred to as "friendly clone" in the original route guide) will work.
Arbitrary code execution
- After reloading the save, go down 1 tile, then right 4 tiles.
- Open Pokédex and scroll to the position of Spearow (0x15).
- This sets the temporary variable $D265 to 0x15. $D266 (wFailedToFlee) should remain 0x00 after reset.
- Notice that where the accessible Pokédex ends will depend on what Pokémon you have seen. In case the only wild Pokémon you have seen is Pidgey, Spearow would be out of reach in new Pokédex mode, so the player will need to go to old Pokédex mode (where Cyndaquil comes after Spearow).
- Close out of Pokédex, go left 4 tiles then up 1 tile to get back to PC.
- This movement sets up an array at $CD70 that stores some pointers for the background map to begin with "D8 9B DA 9B DC 9B ...".
- Open PC and go to the withdraw screen.
- The game will try to show the unterminated nickname starting from $D073. There are a lot of data between there and $D265, but after a reset they are predictable, and won't contain any terminators or other problematic control characters. Finally, after a few B presses for control characters, the text engine reaches $D265 and triggers 0x1500 control code arbitrary code execution, making the program counter jump to $CD52.
- Relevant register states:
- a = 0x52 (temporary variable for reading out the jump destination $CD52)
- e = 0xFF (jumptable index minus one)
- carry flag unset (calculating the address of jumptable entry 255 didn't overflow)
|Program counter||Hex||ASM||In-game meaning||In-game value||Comments|
|$CD52 – $CD6F||00 (*30)||nop|
|$CD70||D8||ret c||Pointers to background map tiles||$9BD8, $9BDA, $9BDC...||Jump not taken|
|$CD71||9B||sbc e||Sets the carry flag (0x52 - 0xFF)|
|$CD72||DA 9B DC||jp c, $DC9B||Jump taken|
|$DC9B – $DC9E||00 (*4)||nop||Unused|
|$DC9F||18 6F||jr 0x6F||Lucky ID||06255 (0x186F)||Jumps to $DD10|
|$DD10||AD / 00||xor l / nop||Held item of second party Pokémon (Cyndaquil)||Berry / nothing|
|$DD11||2B||dec hl||First move of Cyndaquil||Leer|
|$DD12||21 6C 00||ld hl, 0x006C||Second to fourth move of Cyndaquil||Tackle, Smokescreen, empty slot||hl = 0x006C|
|$DD15||26 FB||ld h, 0xFB||Original Trainer ID of Cyndaquil||09979 (0x26FB)||hl = 0xFB6C|
|$DD17 – $DD18||00 00||nop||Experience of Cyndaquil
|$DD19||CD 00 32||call $3200||Calls WaitBGMap2, which calls DelayFrames;|
a = 0x00, c = 0x00
|Stat experience (HP) of Cyndaquil||50 (0x0032)|
|$DD1C||00||nop||Stat experience (Attack) of Cyndaquil||65 (0x0041)|
|$DD1D||41||ld b, c||b = 0x00|
|$DD1E||00||nop||Stat experience (Defense) of Cyndaquil||64 (0x0040)|
|$DD1F||40||ld b, b|
|$DD20||00||nop||Stat experience (Speed) of Cyndaquil||43 (0x002B)|
|$DD21||2B||dec hl||hl = 0xFB6B|
|$DD22||00||nop||Stat experience (Special) of Cyndaquil||44 (0x002C)|
|$DD23||2C||inc l||hl = 0xFB6C|
|$DD24||E9||jp hl||First byte of DVs of Cyndaquil||0xE9||Jumps to 0xFB6C (Echo RAM, equivalent to $DB6C)|
|$FB6C – $FB71||00 (*6)||nop||Unused|
|$FB72||00||nop||Index of current box (0-based)||Box 1|
|$FB73 – $FB74||00 00||nop||Unused|
|$FB75||93||sub e||Box 1 name||T||a = 0x01|
|$FB76||F6 D6||or a, 0xD6||0 'v||a = 0xD7|
|$FB78||EA 8D FB||ld [$FB8D], a||é N 5||($FB8D) = 0xD7|
|$FB7B||D6 D4||sub a, 0xD4||'v 's||a = 0x03|
|$FB7D||50||ld d, b||(terminator)|
|$FB7E||EA AD FC||ld [$FCAD], a||Box 2 name||é n 6||[wBackupMapGroup] = 0x03|
|$FB81||D6 A5||sub a, 0xA5||'v f||a = 0x5E|
|$FB83||EA B1 FB||ld [$FB91], a||é R 5||($FB91) = 0x5E|
|$FB86||50||ld d, b||(terminator)|
|$FB87||A5||and l||Box 3 name ($FB8D overwritten by previous code)||f||a = 0x4C|
|$FB88||EA AE FC||ld [$FCAE], a||é o 6||[wBackupMapNumber] = 0x4C|
|$FB8B||AF||xor a||p||a = 0x00; clears the carry flag|
|$FB8C||EA D7 FC||ld [$FCD7], a||é (0xD7) 6||[wPartyCount] = 0x00|
|$FB8F||50||ld d, b||(terminator)||d = 0x00|
|$FB90||EA 5E FB||ld [$FB5E], a||Box 4 name ($FB91 overwritten by previous code)||é (0x5E) 5||[wEventFlags + 236] = 0x00|
|$FB93||E1||pop hl||PK||Pops text pointer|
|$FB94||E1||pop hl||PK||Pops return pointer in RunMobileScript|
|$FB94||D0||ret nc||'d||Returns to MobileScriptChar|
- The text engine continues to try to display text from de = 0x00FF, prints some more garbage, but eventually encounters a 0x50 terminator.
- Close the PC, and go downstairs.
- Since the 2nd floor of Pokémon centers are in fact a shared map, the downstairs warp uses wBackupMapGroup and wBackupMapNumber to determine which map to return to. Map 0x4C in map group 0x03 is Red's room in Mt. Silver. Before the ACE, the game is supposed to return the player to warp 0x03 in the 1st floor of Cherrygrove Pokémon Center, so now the game will try to return the player to warp 0x03 in Red's room. Warp 0x03 is invalid, but it turns out to be at coordinate (13, 7), which is conveniently close to Red at (9, 10).
- Talk to Red.
- Normally, Red only appears in Mt. Silver after the Hall of Fame sequence, but setting [wEventFlags + 236] = 0x00 resets the appropriate event flag and lets him appear.
- At this point, we have no Pokémon in our party (more precisely, our party count is 0), so the "instant victory effect" is triggered. Since the player never entered a battle since loading the save file, the overworld script acts as if the player won. In the case of Red, this means going to the credit roll.