-
Notifications
You must be signed in to change notification settings - Fork 964
[ARCHIVED] Bugs and Glitches
NOTICE: This document has not been vetted by the maintainers of the project and will be deleted in the future.
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.
-
Multi-player battle engine
- Moves that have 100% accuracy will miss in 1/256 uses
- Moves that have a 100% chance to critical hit will not crit in 1/256 uses
- Focus Energy quarters the critical hit chance instead of quadrupling it when used
- Substitute may leave the user with 0 HP after it's used
- Dual-type move effectiveness may be misreported
- HP draining moves and Dream Eater may hit when they shouldn't
- PP restoring items do not account for PP Ups when used
- Unexpected Counter damage
- Bide damage doesn't get cleared properly in link battles if you are the host
- Struggle may not function correctly if any move has at least one PP Up
- Psychic/Psywave/Night Shade's animation doesn't wiggle the top 3 screen lines
- Psywave can desync a link battle
- Fly and Dig do not remove the invulnerable status when prevented from reaching their second stage by paralysis or confusion damage
- Healing moves will fail if max HP is 255 or 511 points higher than current HP
- Switch-out messages do not account for underflow
- Haze can prevent a Pokémon from attacking after curing freeze
-
Single-player battle engine
- CoolTrainerFAI switches all the time at 10-20% health instead of 25%
- Blaine uses Super Potion even when his Pokémon aren't below 10% health
- Transformed Pokémon are assumed to be Ditto
- 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
- Ghost Pokémon can be identified without the Silph Scope
- Move swap sound is played in the wrong bank
- Exp. All gives half of the EXP of one participant instead of all participants
- Status-curing items remove stat modifiers
- AI trainer HUD does not update when it uses healing items
- Move swaps disallowed while transformed
-
Game engine
- Cinnabar Island's left-facing shore tiles point to invalid Pokémon
- Star grass tiles don't yield any Pokémon encounters
- Lt. Surge's gym trash cans do not use the proper trash cans for the locks
- Having a stack of 99 items and adding more can cause memory corruption
- Bicycle clerk causes text to appear instantly
- A sign in Route 16 isn't readable at its front
- A cuttable tree can return and block the player like it was never cut
- Invisible PCs and Bench Dudes exist at the Celadon Hotel and most of the Safari Zone rest houses
- The player can jump a ledge to land on top of an NPC
- Falling through a hole on the Bicycle doesn't reset the music
- Using the Escape Rope shows letters or JP characters instead of the proper sprite on DMG and doesn't spin correctly on SGB
- The Item Finder won't detect items at X or Y coordinate 0
- NPCs on the overworld aren't restricted correctly
- NPCs can treat the bottom row or the rightmost column of a map as offscreen
- NPC movement delay can be higher than it should be
- NPCs can randomly load at the corner of the screen when you first enter an area
- NPCs can be stopped by holding down A at the left side of the Route 12 gate binoculars
- Trainers' end battle text 2 isn't read correctly
- Random items can cause Pokémon to evolve
- Erroneous stone evolutions can cause Pokémon to evolve
- Using the Pokédoll on the ghost Marowak can allow you to sequence break
- Glitch Pokémon can corrupt SRAM
- Glitch moves can have variable PP
- Smoke puffs from Strength boulders don't show up correctly
- Fainted parties can be walked around with through resets if poisoned
CollisionCheckOnWater
doesn't properly check for whether to Surf or notGetBattleTransitionID_IsDungeonMap
fails to recognize some maps as dungeon maps- The slot machine's tile loading routine loads too many tiles
- The lucky slot machine in the Game Corner doesn't stop when it should if you get a 7
- 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
- The hidden 40-coin stash in the Game Corner only gives half
- The splash screen adds 2 more stars than it should
- The PC screen in the healing machine doesn't flash correctly
GetName
applies to all names rather than only item nameswd732
isn't cleared when starting a new game at the Cycling Road- Bad emulators cause the 'ED' tile to not display correctly
- The player can escape from the Safari Zone by resetting the game or via poison damage
- NPCs can receive the wrong movement byte and behave incorrectly
- Tile collision detection while on certain tiles causes bad performance and strange behaviour if more tiles are added to collision arrays
- Fix Trainer Fly Glitch
- Disable fishing and surfing in statues
- Stuck in the wall when following Oak to his Lab
-
Graphics
- Sliding of trainer and Pokémon graphics can cause tearing
- The lower-right tile of Pokémon backsprites are deleted when sliding offscreen
- Minimize and Substitute can cause sprite glitches with enemy Pokémon
- OAM updates can be interrupted by V-Blank
- Trainer Card transition screens can show brief garbage on DMG
- Double Edge looks weird when the opponent uses it
-
Audio
- The battle victory music can sometimes play at the wrong time
- Prof. Oak's lab music can sometimes play with a channel cut off
- The 'acquired an item' jingle can sometimes be cut off
- The audio engine may borrow from the high byte of the wrong frequency
- 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
- The Prof. Oak introduction uses Nidorina's cry instead of Nidorino's
- Text
-
Scripted events
- The lucky slot machine in the Game Corner can be the nonexistent slot machine 255 (-1)
- The player doesn't face the guard in the Route 8 gate when stopped by him
- The Lift Key can be immediately grabbed after the Rocket Grunt drops it
- The Silph Co. elevator can exhibit strange behavior on the 11th floor
- Saving Mr. Fuji and warping to his house doesn't let you immediately leave
PokemonTower2FRivalEncounterEventCoords
doesn't have a proper ending terminator
- Internal engine routines
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
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 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
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
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 (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
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
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!
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, 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
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
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]
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
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
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
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
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
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
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
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
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
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
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
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
...
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
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
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
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
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
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.
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
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.
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
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
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
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 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
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
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
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
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)
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
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
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
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
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:
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
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
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])
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 $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
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 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 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 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
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
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
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
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 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.
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
...
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.
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
...
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
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
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 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
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
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
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
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 '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
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
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
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!"
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 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
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
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 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
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 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
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
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
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