Pokémon Blue any% No Save Corruption speedrun route

From Glitch City Wiki
Jump to navigation Jump to search

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.

Preparations

  • 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.
    • Charmander and Bulbasaur has different manipulations.
    • Low Special DV enables it to die to a Pikachu in one hit, and high DVs in other stats help it fight multiple Bug Catchers in the endgame.
  • 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.

Trainer escape

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)
Program counter Hex ASM In-game meaning In-game value Comments
$F8FF – $F9AB 00 (*173) nop Main data of enemy Pokémon 3–6 (empty)
$F9AC AC xor h OT name of enemy Pokémon 1
(Actually the player's name)
m a = 0x07
$F9AD E2 ld [$FF00+c], a MN ($FFF0) = 0x07
$F9AE A0 and b a a = 0x00, f = 0xA0 (zero and half-carry flags set)
$F9AF F2 ld a, [$FF00+c] . a = 0x07
$F9B0 F5 push af Pushes 0x07A0 onto the stack
$F9B1 B3 or e t a = 0x8F
$F9B2 85 add l F a = 0x8E
$F9B3 50 ld d, b (terminator)
$F9B4 – $F9B6 00 (*3) nop (empty)
$F9B7 AC xor h OT name of enemy Pokémon 2
(Actually the player's name)
m a = 0x76
$F9B8 E2 ld [$FF00+c], a MN ($FFF0) = 0x76
$F9B9 A0 and b a a = 0x00, f = 0xA0 (zero and half-carry flags set)
$F9BA F2 ld a, [$FF00+c] . a = 0x76
$F9BB F5 push af Pushes 0x76A0 onto the stack
$F9BC B3 or e t
$F9BD 85 add l F
$F9BE 50 ld d, b (terminator)
$F9BF – $F9C1 00 (*3) nop (empty)
$F9C2 – $F9ED 00 (*44) nop OT names of enemy Pokémon 3–6 (empty)
$F9EE – $FA2F 00 (*66) nop Nicknames of enemy Pokémon 1–6 (empty)
$FA30 51 ld d, c Trainer header pointer $5142
$FA31 42 ld b, d
$FA32 – $FA38 00 (*7) nop Unused at this point
$FA39 08 00 00 ld [$0000], sp Current meta-map script ID 8
Unused
$FA3C – $FA40 00 (*5) nop Unused
$FA41 ?? ?? In-game timer (hours) Certainly 00 (nop) for a speedrun
$FA42 ?? ?? (maxed?)
$FA43 ?? ?? (minutes) World record is 10; all values <= 15 are safe
$FA44 ?? ?? (seconds) Pray for safe values
$FA45 ?? ?? (frames)
$FA46 – $FA7F 00 (*58) nop Safari data, Daycare data, Unused (empty)
$FA80 01 ?? FF ld bc, 0xFF?? Current box Pokémon count 1
Species of current box Pokémon 1 Starter species
Species of current box Pokémon 2 0xFF terminator
$FA83 – $FA95 00 (*19) nop Species of current box Pokémon 3–21 (empty)
$FA96 – $FAA1 ?? (*12) (safe) Main data of current box Pokémon 1 (safe) Should contain only safe values for level 5–6 starter with full HP
$FAA2 F1 pop af Original Trainer ID of current box Pokémon 1 61896 (0xF1C8) a = 0x76, f = 0xA0
$FAA3 C8 ret z Returns to the top address on stack, $07A0

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.

YouTube video

YouTube video by Pokeguy84


See also