Skip to content

[ARCHIVED] Bugs and Glitches

Mord edited this page Oct 2, 2024 · 15 revisions

NOTICE: This document has not been vetted by the maintainers of the project and will be deleted in the future once better bug documentation is created.


Bugs and Glitches

These are known bugs and glitches in the original Pokémon Red and Blue games: code that clearly does not work as intended, or that only works in limited circumstances but has the possibility to fail or crash.

Fixes are written in the diff format. If you've used Git before, this should look familiar:

 this is some code
-delete - lines
+add + lines

Fixes in the multi-player battle engine category will break compatibility with standard Pokémon Red/Blue/Yellow for link battles, unless otherwise noted.

Contents

Multi-player battle engine

Moves that have 100% accuracy will miss in 1/256 uses

Moves that are coded as having 100% accuracy, will miss roughly 0.4% of the time making the accuracy in reality ~99.6%. This is because the game checks if the random number is less than the accuracy value, meaning it fails if they are equal.

Fix: Edit MoveHitTest.doAccuracyCheck in engine/battle/core.asm:

.doAccuracyCheck
; if the random number generated is greater than or equal to the scaled accuracy, the move misses
; note that this means that even the highest accuracy is still just a 255/256 chance, not 100%

+	; The following snippet is taken from Pokemon Crystal, it fixes the above bug.
+	ld a, b
+	cp $FF ; Is the value $FF?
+	ret z ; If so, we need not calculate, just so we can fix this bug.

	call BattleRandom
	cp b
	jr nc, .moveMissed
	ret

Moves that have a 100% chance to critical hit will not crit in 1/256 uses

For the same reason as the above bug, even if a Pokémon would have a 100% chance for a critical hit, it will have a 1/256 chance to miss.

Fix: Edit CriticalHitTest in engine/battle/core.asm:

.SkipHighCritical
+	ld a, b
+	inc a ; optimization of "cp $ff"
+	jr z, .guaranteedCriticalHit
	call BattleRandom            ; generates a random value, in "a"
	rlc a
	rlc a
	rlc a
	cp b                         ; check a against calculated crit rate
	ret nc                       ; no critical hit if no borrow
+.guaranteedCriticalHit
	ld a, $1
	ld [wCriticalHitOrOHKO], a   ; set critical hit flag
	ret

Focus Energy quarters the critical hit chance instead of quadrupling it when used

Focus Energy quarters the critical hit chance when it should be quadrupling the critical hit chance instead because the bits are being shifted in the wrong direction for the effect to work properly. It is likely that the incorrect conditional jump was used, meaning that this lowered critical hit rate was meant to be the normal rate.

Fix: Edit CriticalHitTest in engine/battle/core.asm:

-	jr nz, .focusEnergyUsed      ; bug: using focus energy causes a shift to the right instead of left,
-	                             ; resulting in 1/4 the usual crit chance
+	jr z, .noFocusEnergyUsed
	sla b                        ; (effective (base speed/2)*2)
-	jr nc, .noFocusEnergyUsed
+	jr nc, .focusEnergyUsed
	ld b, $ff                    ; cap at 255/256
-	jr .noFocusEnergyUsed
+	jr .focusEnergyUsed
-.focusEnergyUsed
+.noFocusEnergyUsed
	srl b
-.noFocusEnergyUsed
+.focusEnergyUsed

Note that this fix will lower the normal critical hit rate. If you would prefer to keep the critical hit rate, instead make the following edits:

.handleEnemy
	ld [wd0b5], a
	call GetMonHeader
	ld a, [wMonHBaseSpeed]
	ld b, a
-	srl b                        ; (effective (base speed/2))
	ldh a, [hWhoseTurn]
	and a
	ld hl, wPlayerMovePower
	ld de, wPlayerBattleStatus2
	jr z, .calcCriticalHitProbability
	ld hl, wEnemyMovePower
	ld de, wEnemyBattleStatus2
.calcCriticalHitProbability
	ld a, [hld]                  ; read base power from RAM
	and a
	ret z                        ; do nothing if zero
	dec hl
	ld c, [hl]                   ; read move id
+	ld hl, HighCriticalMoves     ; table of high critical hit moves
+.Loop
+	ld a, [hli]                  ; read move from move table
+	cp c                         ; does it match the move about to be used?
+	jr z, .HighCritical          ; if so, the move about to be used is a high critical hit ratio move
+	inc a                        ; move on to the next move, FF terminates loop
+	jr nz, .Loop                 ; check the next move in HighCriticalMoves
+	srl b                        ; /2 for regular move
+	jr .SkipHighCritical         ; continue as a normal move
+.HighCritical
+	sla b                        ; *2 for high critical hit moves
+	jr nc, .noCarry
+	ld b, $ff                    ; cap at 255/256
+.noCarry
+	sla b                        ; *4 for high critical move
+	jr nc, .SkipHighCritical
+	ld b, $ff
+.SkipHighCritical
	ld a, [de]
	bit GETTING_PUMPED, a        ; test for focus energy
-	jr nz, .focusEnergyUsed      ; bug: using focus energy causes a shift to the right instead of left,
-	                             ; resulting in 1/4 the usual crit chance
+	jr z, .noFocusEnergyUsed
-	sla b                        ; (effective (base speed/2)*2)
+	sla b                        ; (effective (base speed*2))
-	jr nc, .noFocusEnergyUsed
+	jr nc, .focusEnergyUsed
	ld b, $ff                    ; cap at 255/256
	jr .noFocusEnergyUsed
.focusEnergyUsed
-	srl b
+	sla b                        ; (effective ((base speed*2)*2))
+	jr nc, .noFocusEnergyUsed
+	ld b, $ff                    ; cap at 255/256
.noFocusEnergyUsed
-	ld hl, HighCriticalMoves     ; table of high critical hit moves
-.Loop
-	ld a, [hli]                  ; read move from move table
-	cp c                         ; does it match the move about to be used?
-	jr z, .HighCritical          ; if so, the move about to be used is a high critical hit ratio move
-	inc a                        ; move on to the next move, FF terminates loop
-	jr nz, .Loop                 ; check the next move in HighCriticalMoves
-	srl b                        ; /2 for regular move (effective (base speed / 2))
-	jr .SkipHighCritical         ; continue as a normal move
-.HighCritical
-	sla b                        ; *2 for high critical hit moves
-	jr nc, .noCarry
-	ld b, $ff                    ; cap at 255/256
-.noCarry
-	sla b                        ; *4 for high critical move (effective (base speed/2)*8))
-	jr nc, .SkipHighCritical
-	ld b, $ff
-.SkipHighCritical

Substitute may leave the user with 0 HP after it's used

Due to an oversight in the substitute health-checking code, in rare circumstances it may leave the user with 0 HP after the substitute is raised.

Fix: Edit SubstituteEffect_ in engine/battle/move_effects/substitute.asm:

.animationEnabled
    call Bankswitch ; jump to routine depending on animation setting
    ld hl, SubstituteText
    call PrintText
+.checkRemainingHP
+   ld a, [wEnemyMonHP+1]
+   and a
+   jr nz, .done ;if there's HP left, we are done
+   ld a, [wEnemyMonHP] ;check HP high byte
+   and a
+   jr nz, .done
+   ld hl, wEnemyMonHp+1
+   set 0, [hl] ;set HP to 1.
+.done
    jpfar DrawHUDsAndHPBars

Dual-type move effectiveness may be misreported

Due to an oversight in the type effectiveness message code, effectiveness messages for dual-typed Pokémon may be misreported by the game due to the second type multiplier overwriting the first.

Fix: Edit AdjustDamageForMoveType.matchingPairFound in engine/battle/core.asm:

	push hl
	push bc
	inc hl
	ld a, [wDamageMultipliers]
	and $80
	ld b, a
	ld a, [hl] ; a = damage multiplier
	ldh [hMultiplier], a
+	and a  ; cp NO_EFFECT
+	jr z, .gotMultiplier
+	cp NOT_VERY_EFFECTIVE
+	jr nz, .nothalf
+	ld a, [wDamageMultipliers]
+	and $7f
+	srl a
+	jr .gotMultiplier
+.nothalf
+	cp SUPER_EFFECTIVE
+	jr nz, .gotMultiplier
+	ld a, [wDamageMultipliers]
+	and $7f
+	sla a
+.gotMultiplier
	add b
	ld [wDamageMultipliers], a

HP draining moves and Dream Eater may hit when they shouldn't

HP draining moves (such as Leech Life and Mega Drain) and Dream Eater may be able to hit a Pokémon with a Substitute currently up when it should only either miss or break the substitute.

Fix: Edit MoveHitTest.swiftCheck in engine/battle/core.asm:

	ld a, [de]
	cp SWIFT_EFFECT
	ret z ; Swift never misses (this was fixed from the Japanese versions)
	call CheckTargetSubstitute ; substitute check (note that this overwrites a)
	jr z, .checkForDigOrFlyStatus
-; The fix for Swift broke this code. It's supposed to prevent HP draining moves from working on Substitutes.
-; Since CheckTargetSubstitute overwrites a with either $00 or $01, it never works.
+	ld a, [de]
	cp DRAIN_HP_EFFECT
	jp z, .moveMissed
	cp DREAM_EATER_EFFECT
	jp z, .moveMissed
.checkForDigOrFlyStatus

PP restoring items do not account for PP Ups when used

Due to an oversight in the code used for PP restoring items like Ethers and Elixirs, PP Ups aren't accounted for and may not show 'no effect' like they should on a move with full PP and any PP Ups used on it.

Fix: Edit ItemUsePPRestore.fullyRestorePP in engine/items/item_effects.asm:

	ld a, [hl] ; move PP
-; Note that this code has a bug. It doesn't mask out the upper two bits, which
-; are used to count how many PP Ups have been used on the move. So, Max Ethers
-; and Max Elixirs will not be detected as having no effect on a move with full
-; PP if the move has had any PP Ups used on it.
+	and %00111111 ; lower 6 bits store current PP
	cp b ; does current PP equal max PP?
	ret z
	jr .storeNewAmount

Unexpected Counter damage

Counter simply doubles the value of wDamage which can hold the last value of damage dealt whether it was from you, your opponent, a switched out opponent, or a player in another battle. This is because wDamage is used for both the player's damage and opponent's damage, and is not cleared out between switching or battles.

Fix: Edit MainInBattleLoop in engine/battle/core.asm to account for the counter damage. This isn't a simple fix as there are lots of scenarios to consider and you don't want to introduce more bugs by fixing this one!

Bide damage doesn't get cleared properly in link battles if you are the host

Due to an oversight, Bide damage doesn't get cleared properly, only clearing the most significant byte and turning the damage value stores into what the damage was modulo (%) 256 instead of 0 like it should be on the host's side of a link battle. The guest's side clears the Bide damage like it should and is unaffected by this bug.

Fix: Edit FaintEnemyPokemon in engine/battle/core.asm:

	xor a
	ld [wPlayerBideAccumulatedDamage], a
+	ld [wPlayerBideAccumulatedDamage + 1], a
	ld hl, wEnemyStatsToDouble ; clear enemy statuses
	ld [hli], a
	ld [hli], a
	ld [hli], a
	ld [hli], a
	ld [hl], a
	ld [wEnemyDisabledMove], a
	ld [wEnemyDisabledMoveNumber], a
	ld [wEnemyMonMinimized], a
	ld hl, wPlayerUsedMove
	ld [hli], a
	ld [hl], a

Struggle may not function correctly if any move has at least one PP Up

Struggle, due to an oversight, doesn't account for any PP Ups on any moves, leaving some Pokémon potentially defenseless without any PP on any moves to use (could also occur with Disable on their only move). This was fixed in Yellow.

Fix: Edit AnyMoveToSelect.allMovesChecked in engine/battle/core.asm:

-	and a ; any PP left?
+	and $3f ; any PP left?
	ret nz ; return if a move has PP left

Psychic/Psywave/Night Shade's animation doesn't wiggle the top 3 screen lines

Hardware limitations prevent the top 3 screen lines from wiggling as they should during the use of Psychic/Psywave/Night Shade. This fix works around that limitation.

Fix: Edit AnimationWavyScreen in engine/battle/animations.asm:

.loop
+	ld a, [hl]
+	ldh [hSCX], a
	push hl
...
.next
	dec c
	jr nz, .loop
	xor a
+	ldh [hSCX], a
	ldh [hWY], a

Psywave can desync a link battle

Due to the way Psywave's RNG works, Psywave can deal 0 damage on the target's side yet can't on the attacker's side, which leads to a link battle desync.

Fix: Edit ApplyAttackToPlayerPokemon.loop in engine/battle/core.asm:

	call BattleRandom
+	and a
+	jr z, .loop
	cp b
	jr nc, .loop
	ld b, a

Fly and Dig do not remove the invulnerable status when prevented from reaching their second stage by paralysis or confusion damage

When using Fly or Dig, if you are hit by confusion damage or fully paralyzed after the first stage, it can cause your Pokémon to become impenetrable until you use Fly or Dig again.

Fix: Edit CheckPlayerStatusConditions.MonHurtItselfOrFullyParalysed in engine/battle/core.asm:

.MonHurtItselfOrFullyParalysed
	ld hl, wPlayerBattleStatus1
	ld a, [hl]
-	; clear bide, thrashing, charging up, and trapping moves such as warp (already cleared for confusion damage)
-	and ~((1 << STORING_ENERGY) | (1 << THRASHING_ABOUT) | (1 << CHARGING_UP) | (1 << USING_TRAPPING_MOVE))
+	; clear bide, thrashing, charging up, trapping moves such as wrap (already cleared for confusion damage), and invulnerable moves
+	and ~((1 << STORING_ENERGY) | (1 << THRASHING_ABOUT) | (1 << CHARGING_UP) | (1 << USING_TRAPPING_MOVE) | (1 << INVULNERABLE))
	ld [hl], a
	ld a, [wPlayerMoveEffect]

And also edit the enemy's counterpart CheckEnemyStatusConditions.monHurtItselfOrFullyParalysed in engine/battle/core.asm:

.monHurtItselfOrFullyParalysed
	ld hl, wEnemyBattleStatus1
	ld a, [hl]
-	; clear bide, thrashing about, charging up, and multi-turn moves such as warp
-	and ~((1 << STORING_ENERGY) | (1 << THRASHING_ABOUT) | (1 << CHARGING_UP) | (1 << USING_TRAPPING_MOVE))
+	; clear bide, thrashing, charging up, trapping moves such as wrap (already cleared for confusion damage), and invulnerable moves
+	and ~((1 << STORING_ENERGY) | (1 << THRASHING_ABOUT) | (1 << CHARGING_UP) | (1 << USING_TRAPPING_MOVE) | (1 << INVULNERABLE))
	ld [hl], a
	ld a, [wEnemyMoveEffect]

Healing moves will fail if max HP is 255 or 511 points higher than current HP

HP restoring moves will fail if the user's max HP is 255 or 511 points higher than their current HP. This is because when the HP values are compared, nothing is done with the result of the high byte comparison.

Fix: Edit HealEffect_ in engine/battle/move_effects/heal.asm

.healEffect
	ld b, a
	ld a, [de]
-	cp [hl] ; most significant bytes comparison is ignored
-	        ; causes the move to miss if max HP is 255 or 511 points higher than the current HP
+	cp [hl]
	inc de
	inc hl
+	jr nz, .passed
	ld a, [de]
	sbc [hl]
	jp z, .failed ; no effect if user's HP is already at its maximum
+.passed
	ld a, b

Switch-out messages do not account for underflow

The code that handles switch-out messages does not account for HP underflow, resulting in strange behavior with the switch-out messages.

Fix: Edit PlayerMon2Text in engine/battle/common_text.asm:

	dec de
	ld b, [hl]
	ld a, [de]
	sbc b
+	jr c, .gainedHP ; if we underflow, print default text
	ldh [hMultiplicand + 1], a
	ld a, 25
	ldh [hMultiplier], a
...
	ldh a, [hQuotient + 3] ; a = ((LastSwitchInEnemyMonHP - CurrentEnemyMonHP) / 25) / (EnemyMonMaxHP / 4)
; Assuming that the enemy mon hasn't gained HP since the last switch in,
; a approximates the percentage that the enemy mon's total HP has decreased
; since the last switch in.
; If the enemy mon has gained HP, then a is garbage due to wrap-around and
; can fall in any of the ranges below.
	ld hl, EnoughText ; HP stayed the same
	and a
	ret z
	ld hl, ComeBackText ; HP went down 1% - 29%
	cp 30
	ret c
	ld hl, OKExclamationText ; HP went down 30% - 69%
	cp 70
	ret c
	ld hl, GoodText ; HP went down 70% or more
	ret
+.gainedHP
+	pop bc
+	pop de
+	ld hl, EnoughText ; default text, a custom message can be used here for this
+	ret

Haze can prevent a Pokémon from attacking after curing freeze

If an enemy Pokémon freezes the player Pokémon while it must recharge from Hyper Beam and then cures the freeze status with Haze, the player will be unable to move for the rest of the battle. This is because, while the Hyper Beam recharge bit is reset when the player inflicts freeze, it is not reset when the enemy does so. Haze loads $ff as the target's last selected move if it is frozen, which prevents it from moving when the player hasn't selected a move. However, because the recharge bit is still set, the player is unable to select a new move, and because $ff is the last selected move, the routine that checks and resets the recharge bit is never run. This permanently locks the player out of making a move.

Fix: Edit FreezeBurnParalyzeEffect.freeze2 in engine/battle/effects.asm:

-; hyper beam bits aren't reseted for opponent's side
+	call ClearHyperBeam
	ld a, 1 << FRZ
	ld [wBattleMonStatus], a
	ld hl, FrozenText
	jp PrintText

Single-player battle engine

CoolTrainerFAI switches all the time at 10-20% health instead of 25%

Due to a bug in the CoolTrainerFAI routines, the CooltrainerF AI will always switch when their current Pokémon out has between 10-20% health rather than only 25% of the time.

Fix: Edit CoolTrainerFAI in engine/battle/trainer_ai.asm:

	; The intended 25% chance to consider switching will not apply.
	; Uncomment the line below to fix this.
	cp 25 percent + 1
-	; ret nc
+	ret nc

Blaine uses Super Potion even when his Pokémon aren't below 10% health

Due to an oversight in Blaine's AI in Red/Blue, the routine that checks if Blaine should use a Super Potion doesn't check for if his Pokémon's health is below 10%, rather uses it 25% of the time regardless of this condition. This was fixed in Yellow using the below fix.

Fix: Edit BlaineAI in engine/battle/trainer_ai.asm:

	cp 25 percent + 1
	ret nc
+	ld a, 10
+	call AICheckIfHPBelowFraction
+	ret nc
	jp AIUseSuperPotion

Transformed Pokémon are assumed to be Ditto

Just like the bug from Generation 2 (webpage version here), Transformed Pokémon are assumed to be Ditto and therefore this is a bug that was carried into Gen 2.

Fix: Edit ItemUseBall.skipShakeCalculations in engine/items/item_effects.asm:

	ld hl, wEnemyBattleStatus3
	bit TRANSFORMED, [hl]
	jr z, .notTransformed
-	ld a, DITTO
-	ld [wEnemyMonSpecies2], a
	jr .skip6

The Pokémon behind the Ghost is identified as seen in the Pokédex even if you didn't use the Silph Scope on it

In a Ghost battle, the Pokémon behind the Ghost will be identified as seen in the Pokédex, 'hinting' at what Pokémon's behind the Ghost.

Note that this fix is a subjective one, fix it if you want to!

Fix: Edit LoadEnemyMonData.copyBaseStatsLoop in engine/battle/core.asm:

	ld a, [hl]     ; base exp
	ld [de], a
	ld a, [wEnemyMonSpecies2]
	ld [wd11e], a
	call GetMonName
	ld hl, wcd6d
	ld de, wEnemyMonNick
	ld bc, NAME_LENGTH
	call CopyData
	ld a, [wEnemyMonSpecies2]
	ld [wd11e], a
	predef IndexToPokedex
+	call IsGhostBattle
+	jr z, .noMarkSeen
	ld a, [wd11e]
	dec a
	ld c, a
	ld b, FLAG_SET
	ld hl, wPokedexSeen
	predef FlagActionPredef ; mark this mon as seen in the pokedex
+.noMarkSeen
	ld hl, wEnemyMonLevel
	ld de, wEnemyMonUnmodifiedLevel
	ld bc, 1 + NUM_STATS * 2
	call CopyData

Ghost Pokémon can be identified without the Silph Scope

In a Ghost battle, if you swap to the party menu or bag and then swap back to battle, the Pokémon behind the ghost can be identified by the player without needing the Silph Scope.

Fix: Edit PartyMenuOrRockOrRun.partyMonWasSelected in engine/battle/core.asm:

	ld a, [wEnemyMonSpecies]
	ld [wcf91], a
	ld [wd0b5], a
	call GetMonHeader
	ld de, vFrontPic
-	call LoadMonFrontSprite
+	call IsGhostBattle
+	push af
+	call nz, LoadMonFrontSprite
+	pop af
+	call z, LoadGhostPic

Edit InitWildBattle.isGhost in engine/battle/core.asm:

+LoadGhostPic:
+	ld hl, wMonHSpriteDim
+	ld a, $66
+	ld [hli], a   ; write sprite dimensions
+	ld bc, GhostPic
+	ld a, c
+	ld [hli], a   ; write front sprite pointer
+	ld [hl], b
+	ld hl, wEnemyMonNick  ; set name to "GHOST"
+	ld a, "G"
+	ld [hli], a
+	ld a, "H"
+	ld [hli], a
+	ld a, "O"
+	ld [hli], a
+	ld a, "S"
+	ld [hli], a
+	ld a, "T"
+	ld [hli], a
+	ld [hl], "@"
+	ld a, [wcf91]
+	push af
+	ld a, MON_GHOST
+	ld [wcf91], a
+	ld de, vFrontPic
+	call LoadMonFrontSprite ; load ghost sprite
+	pop af
+	ld [wcf91], a
+	ret
InitWildBattle:
	ld a, $1
	ld [wIsInBattle], a
	call LoadEnemyMonData
	call DoBattleTransitionAndInitBattleVariables
	ld a, [wCurOpponent]
	cp RESTLESS_SOUL
	jr z, .isGhost
	call IsGhostBattle
	jr nz, .isNoGhost
.isGhost
+	call LoadGhostPic
-	ld hl, wMonHSpriteDim
-	ld a, $66
-	ld [hli], a   ; write sprite dimensions
-	ld bc, GhostPic
-	ld a, c
-	ld [hli], a   ; write front sprite pointer
-	ld [hl], b
-	ld hl, wEnemyMonNick  ; set name to "GHOST"
-	ld a, "G"
-	ld [hli], a
-	ld a, "H"
-	ld [hli], a
-	ld a, "O"
-	ld [hli], a
-	ld a, "S"
-	ld [hli], a
-	ld a, "T"
-	ld [hli], a
-	ld [hl], "@"
-	ld a, [wcf91]
-	push af
-	ld a, MON_GHOST
-	ld [wcf91], a
-	ld de, vFrontPic
-	call LoadMonFrontSprite ; load ghost sprite
-	pop af
-	ld [wcf91], a
	jr .spriteLoaded

Move swap sound is played in the wrong bank

Due to an oversight, the move swap sound is played from the wrong bank, playing the wrong sound as a result. Fixed in Yellow.

Fix: Edit OneTwoAndText in engine/pokemon/learn_move.asm:

	text_far _OneTwoAndText
	text_pause
	text_asm
+	push af
+	push bc
+	push de
+	push hl
+	ld a, $1
+	ld [wMuteAudioAndPauseMusic], a
+	call DelayFrame
+	ld a, [wAudioROMBank]
+	push af
+	ld a, BANK(SFX_Swap_1)
+	ld [wAudioROMBank], a
+	ld [wAudioSavedROMBank], a
+	call WaitForSoundToFinish
	ld a, SFX_SWAP
-	call PlaySoundWaitForCurrent
+	call PlaySound
+	call WaitForSoundToFinish
+	pop af
+	ld [wAudioROMBank], a
+	ld [wAudioSavedROMBank], a
+	xor a
+	ld [wMuteAudioAndPauseMusic], a
+	pop hl
+	pop de
+	pop bc
+	pop af
	ld hl, PoofText
	ret

Exp. All gives half of the EXP of one participant instead of all participants

Due to an oversight in the Exp. All code, the Exp. All gives half of the EXP of only one participant instead of half the EXP of all participants to each Pokémon in the party.

Fix: Store the number of non-fainted Pokémon participants earlier in the Exp. All code, multiply by the amount of non-fainted members, and store the experience back in the wEnemyMonBaseStats structure in the proper locations after this has completed.

jojobear13's full explanation of the fix

Status-curing items remove stat modifiers

Using an item that cures a Pokémon's status conditions will reset their stats back to their neutral, unmodified values. This is done to remove the stat changes caused by burn and paralysis. However, the modifications from stat changing moves, as well as badge boosts, do not get reapplied. In addition, the number of stat stages does not get updated, which could cause stat changing moves to not behave properly if a stat was at either the +6 or -6 cap.

Fix: Edit ItemUseMedicine.checkMonStatus in engine/items/item_effects.asm:

	ld de, wBattleMonStats
	ld bc, NUM_STATS * 2
	call CopyData ; copy party stats to in-battle stat data
-	predef DoubleOrHalveSelectedStats
+	xor a
+	ld [wCalculateWhoseStats], a
+	callfar CalculateModifiedStats
+	callfar ApplyBadgeStatBoosts
	jp .doneHealing

AI trainer HUD does not update when it uses healing items

When an AI trainer uses a Full Heal (such as Brock), the status of the opposing pokemon does not update until after the player's turn. When playing with Super Gameboy colors, and the opponent is in yellow or red health, and the AI trainer uses a healing item to restore HP up into a new color (such as back into green health), the color of the HP bar will not update until after the player's turn. The AI's version of using a Full Restore combines both of these observations. Why does this happen? The cause is that, unlike what is done in the core battle engine, the function DrawEnemyHUDAndHPBar never gets run when executing the AI item functions.

Fix: Edit AIPrintItemUseAndUpdateHPBar and AICureStatus in engine/battle/trainer_ai.asm:

AICureStatus:
; cures the status of enemy's active pokemon
	ld a, [wEnemyMonPartyPos]
	ld hl, wEnemyMon1Status
	ld bc, wEnemyMon2 - wEnemyMon1
	call AddNTimes
	xor a
	ld [hl], a ; clear status in enemy team roster
	ld [wEnemyMonStatus], a ; clear status of active enemy
	ld hl, wEnemyBattleStatus3
	res 0, [hl]
+	push af
+	farcall DrawEnemyHUDAndHPBar
+	pop af
	ret
AIPrintItemUseAndUpdateHPBar:
	call AIPrintItemUse_
	hlcoord 2, 2
	xor a
	ld [wHPBarType], a
	predef UpdateHPBar2
+	push af
+	farcall DrawEnemyHUDAndHPBar
+	pop af
	jp DecrementAICount

Move swaps disallowed while transformed

Never try to swap moves with SELECT while transformed... For some reason, strange things happen...

engine/battle/core.asm:

...
	text_end

SwapMovesInMenu:
+	ld a, [wPlayerBattleStatus3]
+	bit TRANSFORMED, a
+	jp nz, MoveSelectionMenu ; No move swapping while transformed

	ld a, [wMenuItemToSwap]
	and a
	jr z, .noMenuItemSelected
...

Game engine

Cinnabar Island's left-facing shore tiles point to invalid Pokémon

Due to an oversight in the localizations of Red/Blue, the left-facing shore tiles of Cinnabar Island will allow players to encounter Pokémon from a glitched table rather than the normal table for the route. This oversight was responsible for the discovery of Missingno. as well as other various glitch Pokémon. This also fixes the below bug and various other 2x2 block encounter glitches, and was fixed in Yellow.

Fix: Edit TryDoWildEncounter.next in engine/battle/wild_encounters.asm:

.next
; determine if wild pokemon can appear in the half-block we're standing in
-; is the bottom right tile (9,9) of the half-block we're standing in a grass/water tile?
+; is the bottom left tile (8,9) of the half-block we're standing in a grass/water tile?
+; note that by using the bottom left tile, this prevents the "left-shore" tiles from generating grass encounters
-	hlcoord 9, 9
+	hlcoord 8, 9

Star grass tiles don't yield any Pokémon encounters

For some reason, in the forest tileset, the extra grass tile, known as 'star grass', has no potential for wild Pokémon encounters like it should. This fix allows you to fix the star grass without fixing the above bug. This was fixed in Yellow.

Fix: Edit TryDoWildEncounter.next in engine/battle/wild_encounters.asm:

	hlcoord 9, 9
	ld c, [hl]
-	ld a, [wGrassTile]
-	cp c
+	call TestGrassTile
	ld a, [wGrassRate]

and add this function (TestGrassTile) below TryDoWildEncounter later in the same file:

TestGrassTile:
	ld a, [wGrassTile]
	cp c
	jr z, .return
	ld a, [wCurMapTileset]
	cp FOREST
	jr nz, .return
	ld a, $34	; check for the extra grass tile in the forest tileset
	cp c
.return
	ret

Lt. Surge's gym trash cans do not use the proper trash cans for the locks

Due to an oversight in the code that randomizes the key locations for Lt. Surge's gym, the first trash can can have a lock if any other trash can has the other lock. The intended behavior here is have the first trash can have the lock only if the second or fourth trash cans have the other lock.

Fix: Edit PrintTrashText.openFirstLock in engine/events/hidden_objects/vermilion_gym_trash.asm:

	ldh [hGymTrashCanRandNumMask], a
	push hl
+.tryagain
	call Random
	swap a
	ld b, a
	ldh a, [hGymTrashCanRandNumMask]
	and b
+	jr z, .tryagain
	dec a
	pop hl

Having a stack of 99 items and adding more can cause memory corruption

When adding another stack of items, such as Great Balls, to a stack where there's already 99 of said item, there's a chance it can overflow the WRAM space reserved for the item buffer and treat unrelated addresses as items.

Fix: Edit AddItemToInventory_.notAtEndOfInventory in engine/items/inventory.asm:

.notAtEndOfInventory
	ld a, [hli]
	ld b, a ; b = ID of current item in table
	ld a, [wcf91] ; a = ID of item being added
	cp b ; does the current item in the table match the item being added?
	jp z, .increaseItemQuantity ; if so, increase the item's quantity
	inc hl
+.checkIfEndOfInventory
	ld a, [hl]
	cp $ff ; is it the end of the table?
	jr nz, .notAtEndOfInventory

And edit AddItemToInventory_.increaseItemQuantity later in the same file:

	ld a, d
	and a ; is there room for a new item slot?
	jr z, .increaseItemQuantityFailed
; if so, store 99 in the current slot and store the rest in a new slot
	ld a, 99
	ld [hli], a
-	jp .notAtEndOfInventory
+	jp .checkIfEndOfInventory
.increaseItemQuantityFailed
	pop hl
	and a
	jr .done

Bicycle clerk causes text to appear instantly

When talking to the clerk that gives you the Bicycle via either the Bike Voucher or paying 1 million Pokédollars, if exited out the text will then appear instantly from then on. This was fixed in Yellow.

Fix: Edit BikeShopText1.asm_41190 in scripts/BikeShop.asm:

	call PrintText
+	ld hl, wd730
+	res 6, [hl]
	call HandleMenuInput
	bit BIT_B_BUTTON, a
	jr nz, .cancel
-	ld hl, wd730
-	res 6, [hl]
	ld a, [wCurrentMenuItem]
	and a
	jr nz, .cancel
	ld hl, BikeShopCantAffordText
	call PrintText

A sign in Route 16 isn't readable at its front

When trying to read the sign at Route 16 showing Celadon <-> Fuchsia, the sign won't be interactable if you read it from the front.

Fix: Edit data/maps/objects/Route17.asm:

	bg_event  9, 87, 14 ; Route17Text14
	bg_event  9, 111, 15 ; Route17Text15
	bg_event  9, 141, 16 ; Route17Text16
+	bg_event  5, -1, 17 ; Route17Text17 which is a repeat of Route16Text9

And edit scripts/Route17.asm:

	dw Route17Text14
	dw Route17Text15
	dw Route17Text16
+	dw Route17Text17
...
Route17Text14:
	text_far _Route17Text14
	text_end

Route17Text15:
	text_far _Route17Text15
	text_end

Route17Text16:
	text_far _Route17Text16
	text_end
+
+Route17Text17:
+	text_far _Route16Text9
+	text_end

Or edit maps/Route16.blk with Polished Map so that the sign is not at the very bottom of the map.

A cuttable tree can return and block the player like it was never cut

The cuttable tree near the map border at Route 14 can in some circumstances 'return' and act like it wasn't cut when it clearly was.

Fix: Edit _GetTileAndCoordsInFrontOfPlayer in engine/overworld/player_state.asm:

	and a ; cp SPRITE_FACING_DOWN
	jr nz, .notFacingDown
; facing down
+	ld a, 8
+	ld [wTempColCoords], a
+	ld a, 11
+	ld [wTempColCoords + 1], a
	lda_coord 8, 11
	inc d
	jr .storeTile
	cp SPRITE_FACING_UP
	jr nz, .notFacingUp
; facing up
+	ld a, 8
+	ld [wTempColCoords], a
+	ld a, 7
+	ld [wTempColCoords + 1], a
	lda_coord 8, 7
	dec d
	jr .storeTile
	cp SPRITE_FACING_LEFT
	jr nz, .notFacingLeft
; facing left
+	ld a, 6
+	ld [wTempColCoords], a
+	ld a, 9
+	ld [wTempColCoords + 1], a
	lda_coord 6, 9
	dec e
	jr .storeTile
	cp SPRITE_FACING_RIGHT
	jr nz, .storeTile
; facing right
+	ld a, 10
+	ld [wTempColCoords], a
+	ld a, 9
+	ld [wTempColCoords + 1], a
	lda_coord 10, 9
	inc e

Then edit .storeTile later in the same file:

+	cp $3d
+	call z, ReadTileFromVram
	ld c, a
	ld [wTileInFrontOfPlayer], a
	ret

And add a new function, ReadTileFromVram, just below the function that was just edited:

ReadTileFromVram:
	;b=X window offset
	;c=Y window offset
	push bc
	ld a, [wTempColCoords]
	ld b, a
	ld a, [wTempColCoords + 1]
	ld c, a
	;get the x offset in vram
	ld a, [rSCX]
	call .div8
	add b
	cp $20
	call nc, .sub20
	ld b, a
	;get the y offset in vram
	ld a, [rSCY]
	call .div8
	add c
	cp $20
	call nc, .sub20
	ld c, a
	;set vram starting address
	push hl
	ld hl, $9800
	;move to proper y coordinate
	push de
	ld de, $0020
.loop	
	sub 1
	jr c, .endloop
	add hl, de
	jr .loop
.endloop
	;move to proper x coordinate
	ld d, $00
	ld e, b
	add hl, de
	pop de
.wait
	ld a, [hl]
	cp $ff
	jr z, .wait
	pop hl
	pop bc
	ret
.div8
	srl a
	srl a
	srl a
	ret
.sub20
	sub $20
	ret

And finally, edit ram/wram.asm:

NEXTU
+wTempColCoords::
	ds 30
wEngagedTrainerClass:: db
wEngagedTrainerSet:: db
ENDU

Invisible PCs and Bench Dudes exist at the Celadon Hotel and most of the Safari Zone rest houses

At the Celadon Hotel, where a PC at the Pokémon Center would be, there's an invisible PC easily accessible and usable. There are also invisible PCs and bench dudes in 3 of the 4 Safari Zone rest houses out of bounds, only accessible by cheating. It's likely the intent was to remove these invisible PCs and bench dudes from their associated maps as they were removed in Yellow.

Fix: Edit CeladonHotelHiddenObjects in data/events/hidden_objects.asm:

-	hidden_object 13,  3, SPRITE_FACING_UP, OpenPokemonCenterPC
	hidden_object  0,  4, SPRITE_FACING_LEFT, PrintBenchGuyText
	db -1 ; end

and delete SafariZoneRestHouse*HiddenObjects, where * is the number of the rest house, later in the same file.

The player can jump a ledge to land on top of an NPC

A quirk in how the ledge collision detection works causes you to be able to land directly on an NPC if you time a ledge jump just right.

Fix: Edit HandleLedges.foundMatch in engine/overworld/ledges.asm:

	ldh a, [hJoyHeld]
	and e
	ret z
+	push de
+	xor a
+	ld [hSpriteIndexOrTextID], a
+	ld d, $20 ; talking range in pixels (double normal range)
+	call IsSpriteInFrontOfPlayer2
+	ld a, [hSpriteIndexOrTextID]
+	and a ; was there a sprite collision?
+	pop de
+	ret nz
	ld a, $ff
	ld [wJoyIgnore], a
	ld hl, wd736
	set 6, [hl] ; jumping down ledge

Falling through a hole on the Bicycle doesn't reset the music

When falling through a hole, if you're riding the bicycle, the music doesn't switch back to the map's music (because you fall off the Bicycle when you fall down the hole).

Fix: Edit LeaveMapThroughHoleAnim in engine/overworld/player_animations.asm:

+	ld a, [wLastMusicSoundID]
+	cp MUSIC_BIKE_RIDING
+	call z, PlayDefaultMusic
	ld a, $ff
	ld [wUpdateSpritesEnabled], a ; disable UpdateSprites
	; shift upper half of player's sprite down 8 pixels and hide lower half
	ld a, [wShadowOAMSprite00TileID]

Using the Escape Rope shows letters or JP characters instead of the proper sprite on DMG and doesn't spin correctly on SGB

When using the Escape Rope on an original Game Boy, for a split second while the player is shooting up into the sky, the player will use ABCD sprites instead of keep spinning like they're supposed to. The Super Game Boy has a similar error where the player doesn't spin as they shoot up, which are two variants of the same bug.

Fix: Edit PlayerSpinWhileMovingDown in engine/overworld/player_animations.asm:

	ld hl, wPlayerSpinWhileMovingUpOrDownAnimDeltaY
	ld a, $10
	ld [hli], a ; wPlayerSpinWhileMovingUpOrDownAnimDeltaY
	ld a, $3c
	ld [hli], a ; wPlayerSpinWhileMovingUpOrDownAnimMaxY
	call GetPlayerTeleportAnimFrameDelay
	ld [hl], a ; wPlayerSpinWhileMovingUpOrDownAnimFrameDelay
+	ld hl, wFacingDirectionList
	jp PlayerSpinWhileMovingUpOrDown

and edit _LeaveMapAnim.spinWhileMovingUp later in the same file:

	ld a, SFX_TELEPORT_EXIT_1
	call PlaySound
	ld hl, wPlayerSpinWhileMovingUpOrDownAnimDeltaY
	ld a, -$10
	ld [hli], a ; wPlayerSpinWhileMovingUpOrDownAnimDeltaY
	ld a, $ec
	ld [hli], a ; wPlayerSpinWhileMovingUpOrDownAnimMaxY
	call GetPlayerTeleportAnimFrameDelay
	ld [hl], a ; wPlayerSpinWhileMovingUpOrDownAnimFrameDelay
+	ld hl, wFacingDirectionList
	call PlayerSpinWhileMovingUpOrDown
	call IsPlayerStandingOnWarpPadOrHole
	ld a, b
	dec a
	jr z, .playerStandingOnWarpPad

The Item Finder won't detect items at X or Y coordinate 0

If a hidden item happens to be placed at X coordinate 0 or Y coordinate 0, the Item Finder will fail to detect this item.

Fix: Edit HiddenItemNear in engine/items/itemfinder.asm:

	ld a, [wYCoord]
	call Sub5ClampTo0
	cp d
+	jr z, .y_zflag
	jr nc, .loop
+.y_zflag
	ld a, [wYCoord]
	add 4
	cp d
	jr c, .loop
	ld a, [wXCoord]
	call Sub5ClampTo0
	cp e
+	jr z, .x_zflag
	jr nc, .loop
+.x_zflag
	ld a, [wXCoord]
	add 5
	cp e
	jr c, .loop
	scf
	ret

And edit Sub5ClampTo0 later in the same file:

-	sub 5
+	sub 4
	cp $f0
	ret c
	xor a
	ret

NPCs on the overworld aren't restricted correctly

Due to an oversight in the overworld NPC sprite restriction code, the checks for checking to ensure the characters don't walk too far don't work correctly, sometimes to hilarious effect.

Fix: Edit CanWalkOntoTile.tilePassableLoop in engine/overworld/movement.asm:

-	; bug: these tests against $5 probably were supposed to prevent
-	; sprites from walking out too far, but this line makes sprites get
-	; stuck whenever they walked upwards 5 steps
-	; on the other hand, the amount a sprite can walk out to the
-	; right of bottom is not limited (until the counter overflows)
-	cp $5
-	jr c, .impassable  ; if [x#SPRITESTATEDATA2_YDISPLACEMENT]+d < 5, don't go
+	cp $E
+	jr nc, .impassable

Then edit .upwards in the same file:

	sub $1
-	jr c, .impassable  ; if [x#SPRITESTATEDATA2_YDISPLACEMENT] == 0, don't go
+	cp $3
+	jr c, .impassable

Then edit .checkHorizontal in the same file:

	ld d, a
	ld a, [hl]         ; x#SPRITESTATEDATA2_XDISPLACEMENT (initialized at $8, keep track of where a sprite did go)
	bit 7, e           ; check if going left (e=$ff)
	jr nz, .left
	add e
-	cp $5              ; compare, but no conditional jump like in the vertical check above (bug?)
+	cp $E
+	jr nc, .impassable
	jr .passable

And finally, edit .left in the same file:

	sub $1
-	jr c, .impassable  ; if [x#SPRITESTATEDATA2_XDISPLACEMENT] == 0, don't go
+	cp $3
+	jr c, .impassable

NPCs can treat the bottom row or the rightmost column of a map as offscreen

NPCs detect the bottom row and/or the rightmost column of maps as offscreen when in reality they're very much on screen.

Fix: Edit CanWalkOntoTile.tilePassableLoop in engine/overworld/movement.asm:

	ld a, [hli]        ; x#SPRITESTATEDATA1_YPIXELS
	add $4             ; align to blocks (Y pos is always 4 pixels off)
	add d              ; add Y delta
-	cp $80             ; if value is >$80, the destination is off screen (either $81 or $FF underflow)
+	cp $81             ; if value is >$81, the destination is off screen (either $82 or $FF underflow)
	jr nc, .impassable ; don't walk off screen
	inc l
	ld a, [hl]         ; x#SPRITESTATEDATA1_XPIXELS
	add e              ; add X delta
-	cp $90             ; if value is >$90, the destination is off screen (either $91 or $FF underflow)
+	cp $91             ; if value is >$91, the destination is off screen (either $92 or $FF underflow)
	jr nc, .impassable ; don't walk off screen

NPC movement delay can be higher than it should be

Due to an off-by-one error, an NPC movement delay value of 0 can wrap around and cause the NPC's movement delay to be roughly 4.3 seconds greater than it should be.

Fix: Edit UpdateSpriteInWalkingAnimation.initNextMovementCounter in engine/overworld/movement.asm:

	ld l, a
	ldh a, [hRandomAdd]
	and $7f
	ld [hl], a                       ; x#SPRITESTATEDATA2_MOVEMENTDELAY:
	                                 ; set next movement delay to a random value in [0,$7f]
-	                                 ; note that value 0 actually makes the delay $100 (bug?)
+	inc [hl]
	dec h ; HIGH(wSpriteStateData1)
	ldh a, [hCurrentSpriteOffset]
	inc a
	ld l, a

NPCs can randomly load at the corner of the screen when you first enter an area

If you've never visited an area before, if you press start just as the area loads, you can see a random person just standing there in the corner of the screen. Also happens to objects. Fixed in Yellow.

Fix: Edit InitializeSpriteStatus in engine/overworld/movement.asm:

	ldh a, [hCurrentSpriteOffset]
	add $2
	ld l, a
	ld a, $8
	ld [hli], a   ; [x#SPRITESTATEDATA2_YDISPLACEMENT] = 8
	ld [hl], a    ; [x#SPRITESTATEDATA2_XDISPLACEMENT] = 8
-	ret

Then edit Route16GateScript1 in scripts/Route16Gate1F.asm:

	ld a, [wSimulatedJoypadStatesIndex]
	and a
	ret nz
	ld a, $f0
	ld [wJoyIgnore], a
+	call UpdateSprites

And edit CheckForHiddenObject.foundMatchingObject in engine/overworld/hidden_objects.asm:

	ld a, [hli]
	ld [wHiddenObjectFunctionArgument], a
	ld a, [hli]
	ld [wHiddenObjectFunctionRomBank], a
	ld a, [hli]
	ld h, [hl]
	ld l, a
+	push hl
+	call UpdateSprites
+	pop hl
	ret

NPCs can be stopped by holding down A at the left side of the Route 12 gate binoculars

If you head to the left side of the binoculars, turn towards them, and press and hold down A, you can stop NPCs from moving when they shouldn't stop moving.

Fix: Edit GateUpstairsScript_PrintIfFacingUp in scripts/Route12Gate2F.asm:

	ld a, [wSpritePlayerStateData1FacingDirection]
	cp SPRITE_FACING_UP
	jr z, .up
-	ld a, TRUE
-	jr .done
+	ld hl, TVWrongSideText
.up
	call PrintText
-	xor a
-.done
-	ld [wDoNotWaitForButtonPressAfterDisplayingText], a
	jp TextScriptEnd

Trainers' end battle text 2 isn't read correctly

Due to a small oversight in the reading and parsing of enemy trainer data, a second end-of-battle text pointer is read yet is overwritten immediately afterwards.

Fix: Edit ReadTrainerHeaderInfo.nonZeroOffset in home/trainers.asm:

	cp $2
	jr z, .readPointer ; read flag's byte ptr
	cp $4
	jr z, .readPointer ; read before battle text
	cp $6
	jr z, .readPointer ; read after battle text
	cp $8
	jr z, .readPointer ; read end battle text
	cp $a
	jr nz, .done
-	ld a, [hli]        ; read end battle text (2) but override the result afterwards (XXX why, bug?)
-	ld d, [hl]
-	ld e, a
-	jr .done
.readPointer
	ld a, [hli]
	ld h, [hl]
	ld l, a

and edit TalkToTrainer.trainerNotYetFought later in the same file:

	ld a, $4
	call ReadTrainerHeaderInfo     ; print before battle text
	call PrintText
	ld a, $a
-	call ReadTrainerHeaderInfo     ; (?) does nothing apparently (maybe bug in ReadTrainerHeaderInfo)
+	call ReadTrainerHeaderInfo     ; read end battle text (2)

Random items can cause Pokémon to evolve

An oversight in the level-up evolution code in Red and Blue can cause random items to be able to evolve Pokémon into glitch Pokémon and vice versa as well as glitch Pokémon into other glitch Pokémon. This was fixed in Yellow.

Fix: Edit EvolutionAfterBattle in engine/pokemon/evos_moves.asm:

	ld [wEvolutionOccurred], a
	dec a
	ld [wWhichPokemon], a
	push hl
	push bc
	push de
+	ld hl, wStartBattleLevels
+	push hl
	ld hl, wPartyCount
	push hl

Then edit Evolution_PartyMonLoop later in the same file:

	ld hl, wWhichPokemon
	inc [hl]
	pop hl
+	pop de
+	ld a, [de]
+	ld [wTempCoins1], a
+	inc de
	inc hl
	ld a, [hl]
	cp $ff ; have we reached the end of the party?
	jp z, .done
	ld [wEvoOldSpecies], a
+	push de
	push hl
	ld a, [wWhichPokemon]
	ld c, a
	ld hl, wCanEvolveFlags

And edit ram/wram.asm:

wSlotMachineWheel2TopTile:: db
wSlotMachineWheel3BottomTile:: db
wSlotMachineWheel3MiddleTile:: db
wSlotMachineWheel3TopTile:: db
+wStartBattleLevels:: ds PARTY_LENGTH ; which is 6 bytes
wPayoutCoins:: dw

Erroneous stone evolutions can cause Pokémon to evolve

Due to an oversight in Red and Blue, erroneous stone evolutions can potentially cause the same effect as with the above bug. This was fixed in Yellow.

Fix: Edit Evolution_PartyMonLoop.checkItemEvo in engine/pokemon/evos_moves.asm:

+	ld a, [wIsInBattle] ; are we in battle?
+	and a
	ld a, [hli]
+	jp nz, .nextEvoEntry1 ; don't evolve if we're in a battle as wcf91 could be holding the last mon sent out
	ld b, a ; evolution item
-	ld a, [wcf91] ; this is supposed to be the last item used, but it is also used to hold species numbers
+	ld a, [wcf91] ; last item used
	cp b ; was the evolution item in this entry used?
	jp nz, .nextEvoEntry1 ; if not, go to the next evolution entry

Using the Pokédoll on the ghost Marowak can allow you to sequence break

The ghost Marowak can be easily skipped by using a Pokédoll on it, which allows you to win the battle and sequence break, never having to acquire the Silph Scope to see the ghost's true form.

Note that this fix is a subjective one, fix it if you want to!

Fix: Edit ItemUsePokedoll in engine/items/item_effects.asm:

	ld a, [wIsInBattle]
	dec a
	jp nz, ItemUseNotTime
	ld a, $01
+	ld [wBattleResult], a
	ld [wEscapedFromBattle], a
	jp PrintItemUseTextAndRemoveItem

Glitch Pokémon can corrupt SRAM

As a side effect of the glitchy sprite Missingno. and other glitch Pokémon have, they can corrupt SRAM and various other sections of memory if not careful. This fix serves as a safeguard against typical corruption of Hall of Fame data among other SRAM sections.

Fix: Edit _UncompressSpriteData in home/uncompress.asm:

	xor a
	ld [wSpriteCurPosX], a
	ld [wSpriteCurPosY], a
	ld [wSpriteLoadFlags], a
	call ReadNextInputByte    ; first byte of input determines sprite width (high nybble) and height (low nybble) in tiles (8x8 pixels)
	ld b, a
-	and $f
+	and $7
+	jr nz, .skip1
+	inc a
+.skip1
	add a
	add a
	add a
	ld [wSpriteHeight], a
	ld a, b
	swap a
-	and $f
+	and $7
+	jr nz, .skip2
+	inc a
+.skip2
	add a
	add a
	add a
	ld [wSpriteWidth], a
	call ReadNextInputBit

Glitch moves can have variable PP

Some glitch moves read from unrelated areas of memory for their PP values, which can appear as variable PP. This fix acts as a safeguard against variable PP, unintended effects of the glitch moves themselves, and more.

Fix: Edit HealParty.pp in engine/events/heal_party.asm:

	push hl
	push de
	push bc

	ld hl, Moves
+	ld de, hl
	ld bc, MOVE_LENGTH
	call AddNTimes

+	ld a, l
+	sub e
+	ld a, h
+	sbc d
+	ld a, 0
+	jr c, .basePP_loaded    ; if HL is < Moves, this is a glitch move and load 0 PP
+	ld de, MovesEndOfList
+	ld a, l
+	sub e
+	ld a, h
+	sbc d
+	ld a, 0
+	jr nc, .basePP_loaded    ; if HL is >= MovesEndOfList, this is a glitch move and load 0 PP
	ld de, wcd6d
	ld a, BANK(Moves)
	call FarCopyData
	ld a, [wcd6d + 5] ; PP is byte 5 of move data

+.basePP_loaded
	pop bc
	pop de
	pop hl

and edit data/moves/moves.asm:

	move SLASH,        NO_ADDITIONAL_EFFECT,        70, NORMAL,       100, 20
	move SUBSTITUTE,   SUBSTITUTE_EFFECT,            0, NORMAL,       100, 10
	move STRUGGLE,     RECOIL_EFFECT,               50, NORMAL,       100, 10
	assert_table_length NUM_ATTACKS
+MovesEndOfList:

Smoke puffs from Strength boulders don't show up correctly

Due to an oversight (more precisely, a developer brainfart in the 90s), the puffs of smoke that show for moving boulders with Strength don't show up correctly.

Fix: Edit AdjustOAMBlockYPos2.loop in engine/battle/animations.asm:

	ld a, [hl]
	add b
	cp 112
	jr c, .skipSettingPreviousEntrysAttribute
-	dec hl
-	ld a, 160 ; bug, sets previous OAM entry's attribute
+	ld a, 160
-	ld [hli], a
.skipSettingPreviousEntrysAttribute
	ld [hl], a
	add hl, de

Fainted parties can be walked around with through resets if poisoned

You can walk around with a fainted party for 3 steps if at least one of them is poisoned with saving and resetting after those three steps are taken. Fixed in Yellow.

Fix: Edit ApplyOutOfBattlePoisonDamage in engine/events/poison.asm:

	call IncrementDayCareMonExp
	ld a, [wStepCounter]
	and $3 ; is the counter a multiple of 4?
-	jp nz, .noBlackOut ; only apply poison damage every fourth step
+	jp nz, .skipPoisonEffectAndSound ; only apply poison damage every fourth step
	ld [wWhichPokemon], a
	ld hl, wPartyMon1Status
	ld de, wPartySpecies

CollisionCheckOnWater doesn't properly check for whether to Surf or not

Due to a small oversight, the check for whether the player needs to surf or not detects collision in a strange and buggy manner. Fixed in Yellow.

Fix: Edit CollisionCheckOnWater in home/overworld.asm:

	ld a, [wPlayerDirection] ; the direction that the player is trying to go in
	ld d, a
	ld a, [wSpritePlayerStateData1CollisionData]
	and d ; check if a sprite is in the direction the player is trying to go
-	jr nz, .checkIfNextTileIsPassable ; bug?
+	jr nz, .collision
	ld hl, TilePairCollisionsWater
	call CheckForJumpingAndTilePairCollisions
	jr c, .collision
	predef GetTileAndCoordsInFrontOfPlayer ; get tile in front of player (puts it in c and [wTileInFrontOfPlayer])

GetBattleTransitionID_IsDungeonMap fails to recognize some maps as dungeon maps

Some maps aren't caught by the dungeon map check when they are obviously dungeon maps, those maps being:

  • The second and third floors of Victory Road
  • Rocket Hideout
  • The first floor of the Pokémon Mansion
  • The first through fourth basement floors of the Seafoam Islands
  • Power Plant
  • Diglett's Cave
  • The ninth through eleventh floors of Silph Co.

Fix: Edit data/maps/dungeon_maps.asm:

-; GetBattleTransitionID_IsDungeonMap fails to recognize
-; VICTORY_ROAD_2F, VICTORY_ROAD_3F, all ROCKET_HIDEOUT maps,
-; POKEMON_MANSION_1F, SEAFOAM_ISLANDS_[B1F-B4F], POWER_PLANT,
-; DIGLETTS_CAVE, and SILPH_CO_[9-11]F as dungeon maps
-

Then edit DungeonMaps1 later in the same file:

	db VIRIDIAN_FOREST
	db ROCK_TUNNEL_1F
	db SEAFOAM_ISLANDS_1F
	db ROCK_TUNNEL_B1F
+	db POKEMON_MANSION_1F
+	db VICTORY_ROAD_2F
+	db VICTORY_ROAD_3F
+	db POWER_PLANT
+	db DIGLETTS_CAVE
	db -1 ; end

And edit DungeonMaps2 also later in the same file:

	; all MT_MOON maps
	db MT_MOON_1F, MT_MOON_B2F
	; all SS_ANNE maps, VICTORY_ROAD_1F, LANCES_ROOM, and HALL_OF_FAME
	db SS_ANNE_1F, HALL_OF_FAME
	; all POKEMON_TOWER maps and Lavender Town buildings
	db LAVENDER_POKECENTER, LAVENDER_CUBONE_HOUSE
	; SILPH_CO_[2-8]F, POKEMON_MANSION[2F-B1F], SAFARI_ZONE, and
	; CERULEAN_CAVE maps, except for SILPH_CO_1F
	db SILPH_CO_2F, CERULEAN_CAVE_1F
+	; SILPH_CO_[9-11]F
+	db SILPH_CO_9F, SILPH_CO_11F
+	; SEAFOAM_ISLANDS_[B1F-B4F]
+	db SEAFOAM_ISLANDS_B1F, SEAFOAM_ISLANDS_B4F
+	; all ROCKET_HIDEOUT maps
+	db ROCKET_HIDEOUT_B1F, ROCKET_HIDEOUT_B4F
	db -1 ; end

The slot machine's tile loading routine loads too many tiles

The slot machine's tile loading routine loads $04 too many tiles when it loads the slot machine tiles for initializing the Game Corner slot machine minigame. This doesn't cause issues during normal play however.

Fix: Edit LoadSlotMachineTiles in engine/slots/slot_machine.asm:

	call DisableLCD
	ld hl, SlotMachineTiles2
	ld de, vChars0
-	ld bc, $1c tiles ; should be SlotMachineTiles2End - SlotMachineTiles2, or $18 tiles
+	ld bc, SlotMachineTiles2End - SlotMachineTiles2
	ld a, BANK(SlotMachineTiles2)
	call FarCopyData2
	ld hl, SlotMachineTiles1
	ld de, vChars2
	ld bc, SlotMachineTiles1End - SlotMachineTiles1
	ld a, BANK(SlotMachineTiles1)
	call FarCopyData2
	ld hl, SlotMachineTiles2
	ld de, vChars2 tile $25
-	ld bc, $1c tiles ; should be SlotMachineTiles2End - SlotMachineTiles2, or $18 tiles
+	ld bc, SlotMachineTiles2End - SlotMachineTiles2
	ld a, BANK(SlotMachineTiles2)
	call FarCopyData2
	ld hl, SlotMachineMap
	decoord 0, 0
	ld bc, SlotMachineMapEnd - SlotMachineMap
	call CopyData
	call EnableLCD
	ld hl, wSlotMachineWheel1Offset

The lucky slot machine in the Game Corner doesn't stop when it should if you get a 7

Due to a small bug, when you get at least one 7 on the wheel, the wheel will still stop randomly like in other slot machines when it's supposed to stop as soon as you get said 7(s).

Fix: Edit SlotMachine_StopWheel1Early.loop in engine/slots/slot_machine.asm:

	ld a, [hli]
	cp HIGH(SLOTS7)
-	jr c, .stopWheel ; condition never true
+	jr z, .stopWheel
	dec c
	jr nz, .loop
	ret

The lucky slot machine in the Game Corner doesn't stop when it should if there are two 7s or BARs on the middle or bottom of the wheel

Due to another, similar yet possible, bug to the above, when you either get two 7s or BARs or the bottom two adjacent wheels get 7s or BARs, the wheel doesn't stop early like it should.

Fix: Edit SlotMachine_StopWheel2Early.sevenAndBarMode in engine/slots/slot_machine.asm:

	call SlotMachine_FindWheel1Wheel2Matches
+	ret nz
	ld a, [de]
	cp HIGH(SLOTSBAR) + 1
-	ret nc
+	jr c, .stopWheel
+	ld a, [wSlotMachineFlags]
+	bit 6, a
+	ret z

The hidden 40-coin stash in the Game Corner only gives half

The hidden stash of 40 coins in the Game Corner only gives you half the coins it's supposed to.

Fix: Edit HiddenCoins in engine/events/hidden_items.asm:

	cp 10
	jr z, .bcd10
	cp 20
	jr z, .bcd20
	cp 40
-	jr z, .bcd20 ; should be bcd40
+	jr z, .bcd40
	jr .bcd100
.bcd10
	ld a, $10
	ldh [hCoins + 1], a
	jr .bcdDone
.bcd20
	ld a, $20
	ldh [hCoins + 1], a
	jr .bcdDone
-.bcd40 ; due to a typo, this is never used
+.bcd40
	ld a, $40
	ldh [hCoins + 1], a
	jr .bcdDone
.bcd100
	ld a, $1
	ldh [hCoins], a
.bcdDone
	ld de, wPlayerCoins + 1
	ld hl, hCoins + 1

The splash screen adds 2 more stars than it should

The splash screen appears to add 2 extra yet invisible stars during the shooting star animation.

Fix: Edit AnimateShootingStar.smallStarsInnerLoop in engine/movie/splash.asm:

	ld a, [wMoveDownSmallStarsOAMCount]
	cp 24
	jr z, .next2
-	add 6 ; should be 4, but the extra 2 aren't visible on screen
+	add 4
	ld [wMoveDownSmallStarsOAMCount], a

The PC screen in the healing machine doesn't flash correctly

The PC screen in the healing machine exhibits some odd behavior when your Pokémon are being healed.

Fix: Edit AnimateHealingMachine in engine/overworld/healing_machine.asm:

	ld de, PokeCenterFlashingMonitorAndHealBall
	ld hl, vChars0 tile $7c
-	lb bc, BANK(PokeCenterFlashingMonitorAndHealBall), 3 ; should be 2
+	lb bc, BANK(PokeCenterFlashingMonitorAndHealBall), 2
	call CopyVideoData
	ld hl, wUpdateSpritesEnabled

GetName applies to all names rather than only item names

Due to a bug in GetName, it'll get TM/HM names for all names instead of only TM/HM names for TMs/HMs.

Fix: Edit GetName in home/names2.asm:

-	; BUG: This applies to all names instead of just items.
-	ASSERT NUM_POKEMON_INDEXES < HM01, \
-		"A bug in GetName will get TM/HM names for Pokémon above ${x:HM01}."
-	ASSERT NUM_ATTACKS < HM01, \
-		"A bug in GetName will get TM/HM names for moves above ${x:HM01}."
-	ASSERT NUM_TRAINERS < HM01, \
-		"A bug in GetName will get TM/HM names for trainers above ${x:HM01}."
+	push bc
+	ld b, a
+	ld a, [wNameListType]
+	cp ITEM_NAME
+	ld a, b
+	pop bc
+	jr nz, .notMachine
	cp HM01
	jp nc, GetMachineName
+.notMachine
	ldh a, [hLoadedROMBank]
	push af
	push hl
	push bc
	push de

wd732 isn't cleared when starting a new game at the Cycling Road

Due to an oversight, the game doesn't clear wd732 when you start a game with your previous save at the Cycling Road. This is a leftover from development when wd732 was responsible for holding various debug flags.

Fix: Edit SetDefaultNames in engine/movie/oak_speech/oak_speech.asm:

 SetDefaultNames:
 	ld a, [wLetterPrintingDelayFlags]
 	push af
 	ld a, [wOptions]
 	push af
+IF DEF(_DEBUG)
 	ld a, [wd732]
	push af
+ENDC
 	ld hl, wPlayerName
... 
 	call FillMemory
+IF DEF(_DEBUG)
 	pop af
 	ld [wd732], a
+ENDC
 	pop af
 	ld [wOptions], a
 	pop af
 	ld [wLetterPrintingDelayFlags], a

Bad emulators cause the 'ED' tile to not display correctly

Due to some emulators' poor coding, the 'ED' tile may not display correctly. Good emulators and real hardware don't exhibit this behavior, however this bugfix is added here for those that may want maximum compatibility with these poorly-written emulators.

Fix: Edit LoadEDTile in engine/menus/naming_screen.asm:

	ld de, ED_Tile
	ld hl, vFont tile $70
-	ld bc, (ED_TileEnd - ED_Tile) / $8
	; to fix the graphical bug on poor emulators
-	;lb bc, BANK(ED_Tile), (ED_TileEnd - ED_Tile) / $8
+	lb bc, BANK(ED_Tile), (ED_TileEnd - ED_Tile) / $8
	jp CopyVideoDataDouble

The player can escape from the Safari Zone by resetting the game or via poison damage

By exiting the Safari Zone to the main gate, answering no to the 'would you like to leave' question, saving once back in the Safari Zone, resetting the game, and leaving the Safari Zone again it is possible to exit the Safari Zone while the game is still counting your steps. This was partially fixed in Yellow.

How this fix tackles that issue is by checking the coordinates that you stand on when return from the Safari Zone and jumping to the 'do you want to leave' code which will guarantee that, no matter the way you come through this exit, the correct thing will happen.

Fix: Edit scripts/SafariZoneGate.asm:

...
.SafariZoneEntranceScript0
	ld hl, .CoordsData_75221
	call ArePlayerCoordsInArray
-	ret nc
+	jr c, .playerInfrontOfClerk
+	ld hl, .exitCoords
+	call ArePlayerCoordsInArray
+	jr c, .SafariZoneEntranceScript5
+	ret
+ .playerInfrontOfClerk
	ld a, $3
...
.CoordsData_75221:
	dbmapcoord  3,  2
	dbmapcoord  4,  2
	db -1 ; end

+.exitCoords
+	dbmapcoord  3,  0
+	dbmapcoord  4,  0
+	db -1 ; end

Additionally you can leave the Safari Zone by having your last Pokémon faint to poison damage, here we'll just use Yellow's fix for that though which basically just tells the game you're not in the Safari Zone anymore if that happens.

Fix: Edit DisplayPlayerBlackedOutText in home/text_script.asm:

...
DisplayPlayerBlackedOutText::
	ld hl, PlayerBlackedOutText
	call PrintText
	ld a, [wd732]
	res 5, a ; reset forced to use bike bit
	ld [wd732], a
+	CheckEvent EVENT_IN_SAFARI_ZONE
+	jr z, .didnotblackoutinsafari
+	xor a
+	ld [wNumSafariBalls], a
+	ld [wSafariSteps], a
+	ld [wSafariSteps + 1], a
+	ResetEvent EVENT_IN_SAFARI_ZONE
+	ld [wcf0d], a
+	ld [wSafariZoneGateCurScript], a
+.didnotblackoutinsafari
	jp HoldTextDisplayOpen

NPCs can receive the wrong movement byte and behave incorrectly

NPCs are defined in map object files for each map in the game. They are assigned movement behaviour, such as WALK or STAY, and a direction, such as LEFT_RIGHT, UP_DOWN, ANY_DIR, etc.

Due to an oversight/error in movement.asm code, subroutine tag UpdateNPCSprite, when finding the address of the NPC to check its movement byte, for NPCs at specific offsets that generate a carry in the below code, we can receive an incorrect movement byte and behave incorrectly seemingly for no reason. If you see NPCs in your game behaving differently than what you assigned to them, this is probably why.

UpdateNPCSprite:
	ldh a, [hCurrentSpriteOffset]
	swap a
	dec a
	add a
	ld hl, wMapSpriteData
	add l ; BUG: there can be a carry from this addition, and it isn't accounted for
	ld l, a
	ld a, [hl]        ; read movement byte 2
	ld [wCurSpriteMovement2], a
	ld h, HIGH(wSpriteStateData1)

wMapSpriteData is the address of the current map's first sprite object data. hCurrentSpriteOffset is the offset in the data of the sprite we are currently looking at. By adding hCurrentSpriteOffset to l, we're supposed to get the offset of the NPC's movement byte. However adding l to hCurrentSpriteOffset can cause a carry to happen, meaning we need to account for this in the h portion of the 16 bit address representing wMapSpriteData. Otherwise we can get an address that points to an arbitrary value that could cause different behaviour than expected.

The fix is simply accounting for the carry in the h portion of the address:

UpdateNPCSprite:
	ldh a, [hCurrentSpriteOffset]
	swap a
	dec a
	add a
	ld hl, wMapSpriteData
	add l
	ld l, a
+;;;;;;;;;;; FIXED: Account for carry
+	jr nc, .nc 
+	inc h
+.nc
+;;;;;;;;;;;
	ld a, [hl]        ; read movement byte 2
	ld [wCurSpriteMovement2], a
	ld h, HIGH(wSpriteStateData1)

Tile collision detection while on certain tiles causes bad performance and strange behaviour if more tiles are added to collision arrays

There are a list of tiles in the game meant for preventing moving between elevations through tiles where this should not be possible. Like northern-facing cave ridges and cave floors. You can find these arrays in the code defined as TilePairCollisionsLand and TilePairCollisionsWater. You might notice strangeness when attempting to add new collision pairs to these arrays. It's due to an oversight in the code. Specifically, within the subroutine CheckForTilePairCollisions. This code loops over these pairs to check if the player should collide with them. However, things get strange when the first tile in a pair matches the tile the player is standing on. This code will get hit:

.currentTileMatchesFirstInPair
	inc hl
-	ld a, [hl]
+	ld a, [hli]
	cp c
	jr z, .foundMatch
	jr .tilePairCollisionLoop

It correctly will detect if a collision should happen with that pair, but after that, the pair list stored in hl register is still pointing to second tile pair if a collision doesn't happen, making every subsequent loop not work correctly. Eventually, due to sheer luck, the loop will end, but often it will do over 100 loops of this collision checking code when you are standing on a tile that is in the list of collision pairs in one of the first tile positions, but doesn't need to generate a collision. The result is a performance hit (although rather unnoticeable) when walking on these tiles, and subsequent pairs in the array not being checked correctly. This luckily doesn't manifest in the vanilla game collision-detection-wise, but if you added more pairs, you might encounter some of them not being processed due to this oversight.

The fix is very simple, change ld a, [hl] in .currentTileMatchesFirstInPair to ld a, [hli]. You can see this was done correctly in the branch of the loop called .currentTileMatchesSecondInPair. It's worth adding even if you don't experience any issues due to the performance improvement.

Fix Trainer Fly Glitch

https://bulbapedia.bulbagarden.net/wiki/Mew_glitch

DISCLAIMER: this fix make the Mew Glitch impossible to be done. In Vanilla, Mew is only obtained in this way.

engine/overworld/clear_variables.asm:

...
	ld hl, wWhichTrade
	ld bc, wStandingOnWarpPadOrHole - wWhichTrade
	call FillMemory
+	; Clear a possible bad game state after a Trainer Fly
+	ld hl, wd730
+	set 3, [hl] ; Tells the trainer encounter script to cancel any pending encounters
+	ld hl, wFlags_0xcd60
+	res 0, [hl] ; Clear encountered trainer flag (avoid blocked buttons after a Trainer Fly)
	ret

home/trainers.asm:

...
.trainerEngaging
	ld hl, wFlags_D733
	set 3, [hl]
+	ld hl, wd730
+	res 0, [hl] ; Clear NPC movement flag to avoid softlock if this trainer doesn't move
+	res 3, [hl] ; Clear Trainer encounter reset flag
	ld [wEmotionBubbleSpriteIndex], a
	xor a ; EXCLAMATION_BUBBLE
	ld [wWhichEmotionBubble], a
...
...
; display the before battle text after the enemy trainer has walked up to the player's sprite
DisplayEnemyTrainerTextAndStartBattle::
+	ld a, [wd730]
+	and $8
+	jp nz, ResetButtonPressedAndMapScript ; Trainer Fly happened, abort this script
	ld a, [wd730]
	and $1
	ret nz ; return if the enemy trainer hasn't finished walking to the player's sprite
...
...
	ldh [hJoyHeld], a
	ldh [hJoyPressed], a
	ldh [hJoyReleased], a
-	ld [wCurMapScript], a               ; reset battle status
+	ld [wCurMapScript], a        ; reset battle status
+	ld hl, wd730
+	res 0, [hl]                  ; Clear NPC movement flag to avoid potential softlocks
+	set 3, [hl]                  ; Set Trainer encounter reset flag to avoid Mew Glitch
+	ld hl, wFlags_0xcd60
+	res 0, [hl]                  ; player is no longer engaged by any trainer
	ret

; calls TrainerWalkUpToPlayer
...

Disable fishing and surfing in statues

In some places the player is able to fish in the statues. This is because the statues in the GYM and DOJO tilesets are considered as shore tiles. The fix presented here was taken from Jojobear's ShinPokered. Make the following changes to engine/items/item_effects.asm.

...
IsNextTileShoreOrWater:
       ld a, [wCurMapTileset]
       ld hl, WaterTilesets
       ld de, 1
       call IsInArray
       jr nc, .notShoreOrWater
+      ld hl, WaterTile
       ld a, [wCurMapTileset]
       cp SHIP_PORT ; Vermilion Dock tileset
-      ld a, [wTileInFrontOfPlayer] ; tile in front of player
+      jr z, .skipShoreTiles ; if it's the Vermilion Dock tileset
+      cp GYM ; eastern shore tile in Safari Zone
+      jr z, .skipShoreTiles
+      cp DOJO ; usual eastern shore tile
       jr z, .skipShoreTiles 
+      ld hl, ShoreTiles
-      cp $48 ; eastern shore tile in Safari Zone
-      jr z, .shoreOrWater
-      cp $32 ; usual eastern shore tile
-      jr z, .shoreOrWater
.skipShoreTiles
+      ld a, [wTileInFrontOfPlayer]
+      ld de, $1
+      call IsInArray
+      jr c, .shoreOrWater
-      cp $14 ; water tile
-      jr z, .shoreOrWater
.notShoreOrWater
       scf
       ret
.shoreOrWater
       and a
       ret

+; shore tiles
+ShoreTiles:
+       db $48, $32
+WaterTile:
+       db $14
+       db $ff ; terminator
+
+; tilesets with water
+WaterTilesets:
+        db OVERWORLD, FOREST, DOJO, GYM, SHIP, SHIP_PORT, CAVERN, FACILITY, PLATEAU
+        db $ff ; terminator
+
-INCLUDE "data/tilesets/water_tilesets.asm"
...

The main idea for the fix is done by skipping the check for shore tiles if we are using the GYM or DOJO tileset. You can also get rid of the file data/tilesets/water_tilesets.asm as it is no longer necessary.

Stuck in the wall when following Oak to his Lab

scripts\PalletTown.asm:

...
	and a ; is the movement script over?
	ret nz

+	; Check and see if we didn't make it to Oak's Lab
+	CheckEvent EVENT_FOLLOWED_OAK_INTO_LAB
+	jr nz, .followed_oak
+	; move player one tile left
+	ld hl, wd736
+	set 1, [hl]
+	ld a, $1
+	ld [wSimulatedJoypadStatesIndex], a
+	ld a, D_LEFT
+	ld [wSimulatedJoypadStatesEnd], a
+	xor a
+	ld [wSpritePlayerStateData1ImageIndex], a
+	jp StartSimulatingJoypadStates

+.followed_oak

	; trigger the next script
	ld a, 5
	ld [wPalletTownCurScript], a
...

Graphics

Sliding of trainer and Pokémon graphics can cause tearing

Tearing will occur when trainer and Pokémon graphics are slid across the screen, in cases of switching out, fainting, trainer dialog, etc.

This is because background transfers are enabled during when these effects are used.

Fix: Edit SlideDownFaintedMonPic in engine/battle/core.asm:

.slideStepLoop ; each iteration, the mon is slid down one row
	push bc
	push de
	push hl
	ld b, 6 ; number of rows
+	xor a
+	ld [hAutoBGTransferEnabled], a
.rowLoop
	push bc
	push hl
	push de
	ld bc, $7
	call CopyData
	dec b
	jr nz, .rowLoop
	ld bc, SCREEN_WIDTH
	add hl, bc
	ld de, SevenSpacesText
	call PlaceString
+	ld a, 1
+	ld [hAutoBGTransferEnabled], a
	ld c, 2
	call DelayFrames
	pop hl
	pop de
	pop bc

as well as SlideTrainerPicOffScreen later in the same file:

	ldh [hSlideAmount], a
	ld c, a
.slideStepLoop ; each iteration, the trainer pic is slid one tile left/right
	push bc
	push hl
	ld b, 7 ; number of rows
+	xor a
+	ld [hAutoBGTransferEnabled], a
.rowLoop
	push hl
	ldh a, [hSlideAmount]
	ld c, a
.nextColumn
	dec c
	jr nz, .columnLoop
	pop hl
	ld de, 20
	add hl, de
	dec b
	jr nz, .rowLoop
+	ld a, 1
+	ld [hAutoBGTransferEnabled], a
	ld c, 2
	call DelayFrames
	pop hl
	pop bc

The lower-right tile of Pokémon backsprites are deleted when sliding offscreen

Due to a slight off-by-one error, Pokémon backsprites have their lower-right corner deleted off screen before it has a chance to be slid off. This was partially fixed in Yellow.

Fix: Edit _AnimationSlideMonOff in engine/battle/animations.asm:

	ld a, [hl]
	add 7
-; This is a bug. The lower right corner tile of the mon back pic is blanked
-; while the mon is sliding off the screen. It should compare with the max tile
-; plus one instead.
-	cp $61
+	cp $62
	ret c
	ld a, " "
	ret
	ld a, [hl]
	sub 7
-; This has the same problem as above, but it has no visible effect because
-; the lower right tile is in the first column to slide off the screen.
-	cp $30
+	cp $31
	ret c
	ld a, " "
	ret

Minimize and Substitute can cause sprite glitches with enemy Pokémon

Sprite weirdness can occur if you use either Minimize or Substitute, look at any random Pokémon in the Pokédex, then exit back into the battle. Minimize's variant of this is turning the enemy sprite into a garbled mess of what you just looked at in the Pokédex, Substitute's turns the enemy sprite into the Substitute sprite. Fixed in Yellow.

Fix: Edit PartyMenuOrRockOrRun.partyMonWasSelected in engine/battle/core.asm:

; display the two status screens
	predef StatusScreen
	predef StatusScreen2
; now we need to reload the enemy mon pic
+	ld a, 1
+	ldh [hWhoseTurn], a
	ld a, [wEnemyBattleStatus2]
	bit HAS_SUBSTITUTE_UP, a ; does the enemy mon have a substitute?
	ld hl, AnimationSubstitute

OAM updates can be interrupted by V-Blank

OAM updates for some maps can be interrupted if V-Blank runs during the OAM update, which can lead to graphical corruption. This fix allows the OAM updater to be skipped during V-Blank.

Fix: Edit UpdateSprites in home/update_sprites.asm:

	ld a, [wUpdateSpritesEnabled]
	dec a
	ret nz
+	ld hl, hSkipOAMUpdates
+	set 0, [hl]
	homecall _UpdateSprites
+	ld hl, hSkipOAMUpdates
+	res 0, [hl]
	ret

Then edit VBlank.ok in home/vblank.asm:

	call AutoBgMapTransfer
	call VBlankCopyBgMap
	call RedrawRowOrColumn
	call VBlankCopy
	call VBlankCopyDouble
	call UpdateMovingBgTiles
+	ld a, [hSkipOAMUpdates]
+	bit 0, a
+	jr nz, .skipOAM
	call hDMARoutine
	ld a, BANK(PrepareOAMData)
	ldh [hLoadedROMBank], a
	ld [MBC1RomBank], a
	call PrepareOAMData
+.skipOAM

And edit ram/hram.asm:

hWhoseTurn:: db ; 0 on player's turn, 1 on enemy's turn

hClearLetterPrintingDelayFlags:: db

-	ds 1
+hSkipOAMUpdates:: db

; bit 0: draw HP fraction to the right of bar instead of below (for party menu)
; bit 1: menu is double spaced
hUILayoutFlags:: db

Trainer Card transition screens can show brief garbage on DMG

The Trainer Card screen on a DMG with a modern IPS LCD screen mod can show a short period of garbage when loading into and out of it due to not having time to load and unload the data.

Fix: Edit StartMenu_TrainerInfo in engine/menus/start_sub_menus.asm:

	call RunPaletteCommand
+	ld a, [wOnSGB]
+	and a
+	call z, Delay3
	call GBPalNormal
	call WaitForTextScrollButtonPress ; wait for button press
	call GBPalWhiteOut
	call LoadFontTilePatterns
	call LoadScreenTilesFromBuffer2 ; restore saved screen
	call RunDefaultPaletteCommand
	call ReloadMapData
+	ld a, [wOnSGB]
+	and a
+	call z, Delay3
	call LoadGBPal

Double Edge looks weird when the opponent uses it

When the player uses double edge, circular orbs come in from the 4 corners of the player's sprite. However when the opponent uses it, they don't- instead they come from strange locations. The fix is very simple. Modify the definition of Subanim_0CirclesCentering in data/battle_anims/subanimations.asm:

Subanim_0CirclesCentering:
-	subanim SUBANIMTYPE_COORDFLIP, 6 ; should be SUBANIMTYPE_HVFLIP
+	subanim SUBANIMTYPE_HVFLIP, 6
	db FRAMEBLOCK_44, BASECOORD_64, FRAMEBLOCKMODE_00
	db FRAMEBLOCK_45, BASECOORD_65, FRAMEBLOCKMODE_00

Audio

The battle victory music can sometimes play at the wrong time

Due to an oversight in the handling of the battle victory music, it can play in some circumstances when you actually lost, for example, when you explode your last Pokémon on a wild one and both faint.

Fix: Edit FaintEnemyPokemon in engine/battle/core.asm:

	hlcoord 0, 0
	lb bc, 4, 11
	call ClearScreenArea
+	call AnyPartyAlive
+	ld a, d
+	and a
+	push af
	ld a, [wIsInBattle]
	dec a
	jr z, .wild_win
	xor a
	ld [wFrequencyModifier], a
	ld [wTempoModifier], a
	ld a, SFX_FAINT_FALL
	call PlaySoundWaitForCurrent
.sfxwait
	ld a, [wChannelSoundIDs + Ch5]
	cp SFX_FAINT_FALL
	jr z, .sfxwait
	ld a, SFX_FAINT_THUD
	call PlaySound
	call WaitForSoundToFinish
	jr .sfxplayed
.wild_win
	call EndLowHealthAlarm
+	pop af
+	push af
	ld a, MUSIC_DEFEATED_WILD_MON
-	call PlayBattleVictoryMusic
+	call nz, PlayBattleVictoryMusic
.sfxplayed
-; bug: win sfx is played for wild battles before checking for player mon HP
-; this can lead to odd scenarios where both player and enemy faint, as the win sfx plays yet the player never won the battle
	ld hl, wBattleMonHP
	ld a, [hli]
	or [hl]
	jr nz, .playermonnotfaint
	ld a, [wInHandlePlayerMonFainted]
	and a ; was this called by HandlePlayerMonFainted?
	jr nz, .playermonnotfaint ; if so, don't call RemoveFaintedPlayerMon twice
	call RemoveFaintedPlayerMon
.playermonnotfaint
-	call AnyPartyAlive
-	ld a, d
-	and a
+	pop af
	ret z

Prof. Oak's lab music can sometimes play with a channel cut off

One of the channels for Prof. Oak's lab music can effectively be stopped before it plays if V-Blank interrupts it.

Fix: Edit OaksLabFollowedOakScript in scripts/OaksLab.asm:

	ld hl, wStatusFlags7
	res BIT_NO_MAP_MUSIC, [hl]
+	call DelayFrame
	call PlayDefaultMusic

The 'acquired an item' jingle can sometimes be cut off

The 'item acquired' jingle can be cut off if it tries to play and the fadeout counter is not 0.

Fix: Edit FoundHiddenItemText in engine/events/hidden_items.asm:

	ld hl, wObtainedHiddenItemsFlags
	ld a, [wHiddenItemOrCoinsIndex]
	ld c, a
	ld b, FLAG_SET
	predef FlagActionPredef
+	ld a, [wAudioFadeOutControl]
+	push af
+	xor a
+	ld [wAudioFadeOutControl], a
	ld a, SFX_GET_ITEM_2
	call PlaySoundWaitForCurrent
	call WaitForSoundToFinish
+	pop af
+	ld [wAudioFadeOutControl], a
	jp TextScriptEnd

The audio engine may borrow from the high byte of the wrong frequency

Due to an oversight in the audio engine code, when the slide variables are initialized, the result of borrowing from the high byte of the wrong frequency may make the result $200 (0x200) greater than it should.

Fix: Edit Audio*_InitPitchSlideVars.targetFrequencyGreater, where * is the number corresponding to the asm file, in audio/engine_1.asm, audio/engine_2.asm, and audio/engine_3.asm:

-; Bug. Instead of borrowing from the high byte of the target frequency as it
-; should, it borrows from the high byte of the current frequency instead.
-; This means that the result will be 0x200 greater than it should be if the
-; low byte of the current frequency is greater than the low byte of the
-; target frequency.
-	ld a, d
-	sbc b
-	ld d, a

+	push af
	ld hl, wChannelPitchSlideTargetFrequencyHighBytes
	add hl, bc
+	pop af
	ld a, [hl]
+	sbc b
	sub d
	ld d, a

Articuno's cry may get distorted when you see it in the binoculars on Route 15/Fossils play their Pokémon's cry when they shouldn't in Pewter Museum

When you see Articuno in the binoculars on Route 15, its cry may be distorted and in some cases may sound like Nidoking. Pewter Museum has a similar problem where the fossils play the respective Pokémon's cry when fossils normally don't make any sound.

Fix: Edit DisplayMonFrontSpriteInBox in engine/events/hidden_objects/museum_fossils.asm:

	ldh [hStartTileID], a
	hlcoord 10, 11
	predef AnimateSendingOutMon
+	ld a, [wcf91]
+	cp FOSSIL_KABUTOPS
+	jr z, .skipCry
+	cp FOSSIL_AERODACTYL
+	jr z, .skipCry
+	call PlayCry
+.skipCry
	call WaitForTextScrollButtonPress
	call LoadScreenTilesFromBuffer1
	call Delay3

And edit Route15GateLeftBinoculars in engine/events/hidden_objects/route_15_binoculars.asm:

	tx_pre Route15UpstairsBinocularsText
	ld a, ARTICUNO
	ld [wcf91], a
-	call PlayCry
	jp DisplayMonFrontSpriteInBox

The Prof. Oak introduction uses Nidorina's cry instead of Nidorino's

Red and Blue show a Nidorino in the introductory speech Prof. Oak gives before you start the game, but the Nidorino uses Nidorina's cry.

Fix: Edit TextCommand_SOUND.play in home/text.asm:

-	cp TX_SOUND_CRY_NIDORINA
+	cp TX_SOUND_CRY_NIDORINO
	jr z, .pokemonCry
	cp TX_SOUND_CRY_PIDGEOT
	jr z, .pokemonCry
	cp TX_SOUND_CRY_DEWGONG
	jr z, .pokemonCry

Then edit TextCommandSounds later in the same file:

	db TX_SOUND_DEX_PAGE_ADDED,       SFX_DEX_PAGE_ADDED
-	db TX_SOUND_CRY_NIDORINA,         NIDORINA ; used in OakSpeech
+	db TX_SOUND_CRY_NIDORINO,         NIDORINO ; used in OakSpeech
	db TX_SOUND_CRY_PIDGEOT,          PIDGEOT  ; used in SaffronCityText12
	db TX_SOUND_CRY_DEWGONG,          DEWGONG  ; unused

Then edit macros/scripts/text.asm:

-	const TX_SOUND_CRY_NIDORINA ; $14
-MACRO sound_cry_nidorina
-	db TX_SOUND_CRY_NIDORINA
+	const TX_SOUND_CRY_NIDORINO ; $14
+MACRO sound_cry_nidorino
+	db TX_SOUND_CRY_NIDORINO
ENDM

And edit OakSpeechText2 in engine/movie/oak_speech/oak_speech.asm:

OakSpeechText2:
	text_far _OakSpeechText2A
-	sound_cry_nidorina
+	sound_cry_nidorino
	text_far _OakSpeechText2B
	text_end

Text

The text used by Prof. Oak when he gives you 5 Pokéballs overwrites the second line with the last line

Due to an oversight when translating the text for Prof. Oak in English Red/Blue, the last line of text used after you speak to Prof. Oak when you get your 5 Pokéballs overwrites the previous line.

Fix: Edit text/OaksLab.asm:

	para "Just throw a #"
-	line "BALL at it and try"
-	line "to catch it!"
+	line "BALL at it and"
+	cont "try to catch it!"

An in-game trade NPC talks about an 'evolving Raichu'

When you trade a Raichu to one of the in-game trade NPCs, he will talk about how said Raichu 'went and evolved.'

Fix: Edit _AfterTrade2Text in data/text/text_7.asm:

	text "The @"
	text_ram wInGameTradeGiveMonName
	text " you"
	line "traded to me"

-	para "went and evolved!"
+	para "has grown strong!"
	done

Alternatively, this fix from Yellow:

-	text "The @"
+	text "Hello there! Your"
+	line "old @"
	text_ram wInGameTradeGiveMonName
-	text " you"
+	text " is"
-	line "traded to me"
+	cont "magnificent!"
-
-	para "went and evolved!"
	done

Or edit TradeMons in data/events/trades.asm:

	db SLOWBRO,    LICKITUNG, TRADE_DIALOGSET_CASUAL, "MARC@@@@@@@"
-	db POLIWHIRL,  JYNX,      TRADE_DIALOGSET_POLITE, "LOLA@@@@@@@"
-	db RAICHU,     ELECTRODE, TRADE_DIALOGSET_POLITE, "DORIS@@@@@@"
+	db POLIWHIRL,  JYNX,      TRADE_DIALOGSET_CASUAL, "LOLA@@@@@@@"
+	db RAICHU,     ELECTRODE, TRADE_DIALOGSET_CASUAL, "DORIS@@@@@@"
	db VENONAT,    TANGELA,   TRADE_DIALOGSET_HAPPY,  "CRINKLES@@@"

The text used by one of the Route 8 battles has text cut off

The text for one of the NPCs that battle you on Route 8 has text that can sound a bit weird to some.

Note that this fix is a subjective one, fix it if you want to!

Fix: Edit _Route8BattleText1 in text/Route8.asm:

	text "You look good at"
	line "#MON, but"
-	cont "how's your chem?"
+	cont "how's your"
+
+	para "chemistry grade?"
	done

Scripted events

The lucky slot machine in the Game Corner can be the nonexistent slot machine 255 (-1)

Due to an off-by-one error, the slot machine that is determined by the game to be the lucky one can be the nonexistent slot machine -1 (or 255).

Fix: Edit GameCornerSelectLuckySlotMachine in scripts/GameCorner.asm:

	ld hl, wCurrentMapScriptFlags
	bit 6, [hl]
	res 6, [hl]
	ret z
	call Random
	ldh a, [hRandomAdd]
-	cp $7
+	cp $8
	jr nc, .not_max
	ld a, $8

The player doesn't face the guard in the Route 8 gate when stopped by him

When the player is stopped by the guard at the Route 8 gate before Cycling Road, the player is facing left, towards the very door the guard is trying to block access to, instead of facing up, directly towards the guard in question.

Fix: Edit Route8GateDefaultScript in scripts/Route8Gate.asm:

Route8GateDefaultScript:
	ld a, [wStatusFlags1]
	bit BIT_GAVE_SAFFRON_GUARDS_DRINK, a
	ret nz
	ld hl, .PlayerInCoordsArray
	call ArePlayerCoordsInArray
	ret nc
-	ld a, PLAYER_DIR_LEFT
+	ld a, PLAYER_DIR_UP
	ld [wPlayerMovingDirection], a
	xor a

The Lift Key can be immediately grabbed after the Rocket Grunt drops it

The Rocket Grunt that drops the Lift Key can be stood in front of and allow the player to grab the Lift Key quickly. Fixed in Yellow.

Fix: Edit RocketHideout4EndBattleText4 in scripts/RocketHideoutB4F.asm:

	text_far _RocketHideout4EndBattleText4
-	text_end
+	text_promptbutton
+	text_asm
+	SetEvent EVENT_ROCKET_DROPPED_LIFT_KEY
+	ld a, HS_ROCKET_HIDEOUT_B4F_ITEM_5
+	ld [wMissableObjectIndex], a
+	predef ShowObject
+	jp TextScriptEnd

and edit RocketHideout4AfterBattleText4 later in the same file:

	ld hl, RocketHideout4Text_455ec
	call PrintText
-	CheckAndSetEvent EVENT_ROCKET_DROPPED_LIFT_KEY
-	jr nz, .asm_455e9
-	ld a, HS_ROCKET_HIDEOUT_B4F_ITEM_5
-	ld [wMissableObjectIndex], a
-	predef ShowObject
-.asm_455e9
	jp TextScriptEnd

The Silph Co. elevator can exhibit strange behavior on the 11th floor

You can reenter the elevator as soon as you get off it on the 11th floor (potentially other floors as well), and the exit mats will work even if you don't move anywhere. Fixed in Yellow.

Fix: Edit data/tilesets/door_tile_ids.asm:

	dbw OVERWORLD,   .OverworldDoorTileIDs
	dbw FOREST,      .ForestDoorTileIDs
	dbw MART,        .MartDoorTileIDs
	dbw HOUSE,       .HouseDoorTileIDs
	dbw FOREST_GATE, .TilesetMuseumDoorTileIDs
	dbw MUSEUM,      .TilesetMuseumDoorTileIDs
	dbw GATE,        .TilesetMuseumDoorTileIDs
	dbw SHIP,        .ShipDoorTileIDs
	dbw LOBBY,       .LobbyDoorTileIDs
	dbw MANSION,     .MansionDoorTileIDs
	dbw LAB,         .LabDoorTileIDs
	dbw FACILITY,    .FacilityDoorTileIDs
	dbw PLATEAU,     .PlateauDoorTileIDs
+	dbw INTERIOR,    .InteriorDoorTileIDs
...
.PlateauDoorTileIDs:
	door_tiles $3b, $1b

+.InteriorDoorTileIDs:
+	door_tiles $04, $15
+

Saving Mr. Fuji and warping to his house doesn't let you immediately leave

Saving Mr. Fuji from the Pokémon Tower doesn't let you immediately leave his house once you warp in unless you move a tile in any direction first.

Fix: Edit PokemonTower7Script4 in scripts/PokemonTower7F.asm:

	ld a, MR_FUJIS_HOUSE
	ldh [hWarpDestinationMap], a
	ld a, $1
	ld [wDestinationWarpID], a
	ld a, LAVENDER_TOWN
	ld [wLastMap], a
+	ld hl, wd736
+	set 2, [hl]
	ld hl, wd72d
	set 3, [hl]
	ld a, $0
	ld [wPokemonTower7FCurScript], a
	ld [wCurMapScript], a
	ret

PokemonTower2FRivalEncounterEventCoords doesn't have a proper ending terminator

In the second floor of the Pokémon Tower, there's some coordinate data that isn't properly terminated and may cause adverse effects ingame.

Fix: Edit PokemonTower2FRivalEncounterEventCoords in scripts/PokemonTower2F.asm:

	dbmapcoord 15,  5
	dbmapcoord 14,  6
-	db $0F ; end? (should be $ff?)
+	db -1 ; end

Internal engine routines

Saves corrupted by mid-save shutoff are not handled

This allows Pokémon to be duplicated, among other effects, like being able to access Pokémon beyond the 6th slot, arbitrary code execution, etc.

Fix: Edit SaveSAV.save in engine/menus/save.asm:

-	call SaveSAVtoSRAM
	hlcoord 1, 13
	lb bc, 4, 18
	call ClearScreenArea
	hlcoord 1, 14
	ld de, NowSavingString
	call PlaceString
	ld c, 120
	call DelayFrames
+	call SaveSAVtoSRAM
	ld hl, GameSavedText
	call PrintText
	ld a, SFX_SAVE

Then edit SaveSAVtoSRAM0 later in the same file:

	ld de, sSpriteData
	ld bc, wSpriteDataEnd - wSpriteDataStart
	call CopyData
+	ld hl, wPartyDataStart
+	ld de, sPartyData
+	ld bc, wPartyDataEnd - wPartyDataStart
+	call CopyData
	ld hl, wBoxDataStart
	ld de, sCurBoxData
	ld bc, wBoxDataEnd - wBoxDataStart

And edit SaveSAVtoSRAM also later in the same file:

	ld a, $2
	ld [wSaveFileStatus], a
-	call SaveSAVtoSRAM0
-	call SaveSAVtoSRAM1
-	jp SaveSAVtoSRAM2
+	jp SaveSAVtoSRAM0

Multiple dialogs are able to be held on-screen with the A button

Various dialogs in-game, such as the save dialog, can be held on-screen until you release the button. This may have been unintentional behavior for these dialogs.

Fix: Edit StartMenu_SaveReset in engine/menus/start_sub_menus.asm:

	ld a, [wd72e]
	bit 6, a ; is the player using the link feature?
	jp nz, Init
	predef SaveSAV ; save the game
	call LoadScreenTilesFromBuffer2 ; restore saved screen
-	jp HoldTextDisplayOpen
+	jp CloseStartMenu

Optional fix: If you want all menus to be closeable with pressing A instead of releasing A, edit AfterDisplayingTextID in home/text_script.asm:

	ld a, [wEnteringCableClub]
	and a
	jr nz, HoldTextDisplayOpen
	call WaitForTextScrollButtonPress ; wait for a button press after displaying all the text
+	jr CloseTextDisplay
Clone this wiki locally