Pokémon Blue any% No Save Corruption speedrun route
The current Pokémon Blue any% No Save Corruption speedrun route is based on glitch meta-map script activation after the "death-warp" variety of the trainer escape glitch. It warps to the credits by arbitrary code execution, which has a chance to fail because the execution flow goes through the in-game timer. Since the arbitrary code execution begins at address $F8FF, this method is also known as "Viridian Forest 0xF8FF glitch".
Two luck manipulations are important for the glitched portion, one to get a specific value of the Trainer ID for the glitch setup, and another to get an encounter on the exact tile in front of a trainer to trigger the death-warp. A third luck manipulation is used to get a Spearow with specific DVs to win or lose the battles faster.
This page documents the route and analyzes the glitches used in detail.
The content of this page is based on the full beginner guide by krazyd4n. Details unrelated to glitches/manipulations (i.e. for time saving only) are omitted.
- If there is an existing save file, clear it by pressing Up + Select + B on the title screen.
- Manipulate for a Trainer ID of 0xF1C8 (61896).
- Name the player character "mMNa.♀tF".
- Choose the starter and name it to suit the needs of the Spearow manipulation later. (Charmander and Bulbasaur have different advantages for speedrunning.)
- Finish the rival fight, deliver Oak's Parcel, and get Poké Balls.
- Manipulate for a Spearow with low Special DV and high DVs in everything else.
- Go to the Pokémon Center, deposit the starter in the PC, and heal to set the death-warp destination.
- It is necessary to deposit something in the PC to make use of the Trainer ID.
- Walk through Viridian Forest, dodging non-mandatory Trainers and using the no encounter grass tiles to minimize wild encounters.
- Manipulate a Pikachu encounter in front of the mandatory trainer ("Weedle Guy"), and lose the battle.
- The luck manipulation can be extended to make Pikachu use Thundershock and land a critical hit, killing Spearow in one turn.
- The trainer will notice the player before the player blacks out. This completes the first part of the trainer escape glitch, setting the meta-map script ID for Viridian Forest to 1.
Glitch meta-map script activation
- Walk back to Viridian Forest without opening the Start menu or any other text box.
- This makes sure that the stored text box ID still corresponds to Weedle Guy. For reasons explained here, this begins the fight with him and sets the meta-map script ID to 4.
- Finish the fight (winning is faster, but losing works too).
- After the fight, meta-map script ID 4 runs, which immediately begins another fight with the same trainer (even if the player has blacked out and warped back to the Pokémon Center) and sets the meta-map script ID to 6 (see the analysis below).
- Finish the second fight (similar to the above fight, both winning and losing works).
- If either of the two fights is lost, the player will be at the Pokémon Center, so walk back to Viridian Forest again.
- The Viridian Forest meta-map script ID is now 6, which is a "walking lag" script.
Meta-map script ID 4 in Viridian Forest is a typical case of "trainer text script as map script". It points to ViridianForestText2:
ViridianForestText2: TX_ASM ; 08 ld hl, ViridianForestTrainerHeader0 ; 21 42 51 call TalkToTrainer ; CD CC 31 jp TextScriptEnd ; C3 D7 24
Reinterpreted as map script:
ld [$4221], sp ; 08 21 42 ld d, c ; 51 call TalkToTrainer ; CD CC 31 jp TextScriptEnd ; C3 D7 24
As usual, since this map script doesn't change hl, ViridianForestText2 itself is reinterpreted again as a trainer header:
db $08 ; bit of "trainer beaten" event flag db $21 ; trainer's view range dw $5142 ; address of "trainer beaten" event flag dw $CCCD ; pointer to text before battle dw $C331 ; pointer to text when talked to after defeated dw $24D7 ; pointer to text when defeated
The event flag points to "bit 8 of $5142" (i.e. bit 0 of $5143) on bank 3. Since the flag is not set, another fight begins, and the meta-map script ID is advanced by 2, ending up at 6.
Meta-map script ID 6 points to ViridianForestText4, which is very similar, except that $5142 (ViridianForestTrainerHeader0) changes to $515A (ViridianForestTrainerHeader2), which results in the "trainer beaten" event flag being set. Therefore it's a "walking lag" script.
Arbitrary code execution
- Walk to the first Bug Catcher in the Forest, and mash A to talk to him.
- Since the current meta-map script is not CheckFightingMapTrainers, he cannot see the player on his own, and the player must press A in-between the invisible "0 ERROR" text boxes to talk to him.
- Talking to a new trainer advances the meta-map script ID by 2 again (up to 8), but the battle has to happen first.
- This battle actually needs to be won, and even then an unlucky IGT can crash the game, so heal up and save if necessary.
- Win the fight.
- This makes sure that the game returns to the overworld before executing meta-map script ID 8, which is important because the setup makes use of the overworld tileset. Losing the battle will cause the meta-map script to run without loading the overworld tileset, which crashes the game.
Meta-map script ID 8 in Viridian Forest points to PickUpItemText at $24F4 (there are actually three copies of pointers to PickUpItemText after ViridianForestText4, corresponding to the three visible item balls in Viridian Forest), which is also a piece of text script:
PickUpItemText:: TX_ASM ; 08 ld a, $5C ; 3E 5C ; 5C is the predef ID for PickUpItem call Predef ; CD 6D 3E jp TextScriptEnd ; C3 D7 24
Reinterpreted as map script:
ld [$5C3E], sp ; 08 3E 5C call Predef ; CD 6D 3E jp TextScriptEnd ; C3 D7 24
Again, this calls the correct function without setting the proper parameter, in this case the a register. It turns out to have the value 0xF4, since it was used as a temporary variable for reading out the jump destination $24F4.
The function Predef is used to run a "predefined function" that might be on another ROM bank, taking care of bank switching. The table of "predef pointers" starts at $13:7E79, and each pointer takes 3 bytes (1 byte of bank + 2 bytes of address), so the "predef pointer ID 0xF4" should live at $8155, except the code to multiply the predef ID by 3 assumes that doubling the ID doesn't generate a carry (reasonable as there are only 98 legitimate predefs), so it instead is read from $8055. Either way, this is no longer in the ROM, but in the VRAM.
VRAM addresses $8055 to $8057 are part of tile 0x05, and with the overworld tileset, their values are "F8 08 F8". If those values are really read as such, then the Predef function should switch to bank $F8 (invalid of course, but not harmful) and then jump to $F808 (echo RAM equivalent of $D808). Unfortunately, at $D89C are the enemy party data, which contains a few 0xFF bytes, and this early in the game they are impossible to dodge. Therefore all $F808 would give is a rst 38 crash.
Fortunately, those values are not actually read as such, because of VRAM inaccessibility. The timing of this map script execution relative to the V-Blank is quite consistent, as long as:
- The in-game timer didn't carry when advanced (i.e. IGT0) on this very frame.
- The audio engine didn't play a note on this very frame.
By some miracle, under the most common timing, the VRAM is inaccessible when reading $8056 (giving a 0xFF), but becomes accessible just before reading $8057 (giving the correct 0xF8). This means that the game instead jumps to $F8FF, nicely dodging the aforementioned 0xFF bytes.
Relevant register states:
- a = 0xFF (bank ID read from $8055)
- hl = 0xF8FF (jump destination)
- b = 0x00 (used as a temporary variable for wMapPalOffset in the beginning of the overworld frame)
- c = 0xF0 (was the sprite offset of the last sprite when updating sprites after returning from the battle)
- de = 0x3E8D (address of Predef.done in ROM; was pushed onto the stack as an return address)
Memory address $07A0 is back in the ROM, and right in the middle of the code that checks for a warp in the overworld. The exact position is:
.goBackOutside ld a, [wLastMap] ld [wCurMap], a ; $07A0 call PlayMapChangeSound xor a ld [wMapPalOffset], a .done ld hl, wd736 set 0, [hl] ; have the player's sprite step out from the door (if there is one) call IgnoreInputForHalfSecond jp EnterMap
As can be seen, the effect of jumping to this position is to warp the player to the map corresponding to the value of the a register. In this case, since a = 0x76 has been set up, the player will be warped to map 0x76, which is the Hall of Fame.