I'm trying to understand a strange bug that happens in Pokemon Red/Blue where if you try using the move Recover when you're exactly 255 hp below your max HP, the move will fail.
The code I am looking at is a disassembly that can be found here. The specific code in question is under the .healEffect label.
I think I've figured out, code-wise, why this is happening. Let's say your max HP is 703, and you are currently at 448. The machine compares your HP values to make sure you aren't trying to heal at full HP. However, the programmers used the "cp" instruction, which is only for 8-bit numbers, cutting off the highest bit. I assume this was just simply programmer error.
703 = 00000010 10111111
448 = 00000001 11000000
Removing the highest bit, your max becomes 191 and current becomes 192.
This isn't possible, so a carry is triggered.
Now where I am confused is why the sbc instruction is used. sbc is called, and the machine performs current HP - max HP - carry. Using the example above, this results in 0, meaning you're at full HP in the machine's eyes, and the move fails.
The cp instruction I can explain as just programmer error. But why on earth use sbc here? sbc is meant to be used for multi-word arithmetic, but HP is a 16-bit number. I'm struggling to think of a situation where sbc is called for in this situation.
Any ideas?
I'm not really an assembly wizard, but I think the issue is that HP might be big-endian, but in this case it's being treated as little-endian. As others have said, it seems that sbc
is used to "borrow" from one 8-bit subtraction to another, allowing 16-bit arithmetic.
In the battle code, when damage is applied, we have damage stored as two bytes at wDamage
and HP stored as two bytes at wBattleMonHP
. Around line 4929 (I call it out below) we subtract the higher memory addresses first and the lower memory addresses second, using the carry flag the second time. This implies that we are "borrowing" from the lower memory addresses, so they should be the ones with the most significant bytes.
ApplyDamageToPlayerPokemon:
ld hl, wDamage
ld a, [hli]
ld b, a
ld a, [hl]
or b
jr z, ApplyAttackToPlayerPokemonDone ; we're done if damage is 0
ld a, [wPlayerBattleStatus2]
bit HAS_SUBSTITUTE_UP, a ; does the player have a substitute?
jp nz, AttackSubstitute
; subtract the damage from the pokemon's current HP
; also, save the current HP at wHPBarOldHP and the new HP at wHPBarNewHP
ld a, [hld] ; <<<<<<< line 4929 >>>>>
ld b, a
ld a, [wBattleMonHP + 1]
ld [wHPBarOldHP], a
sub b
ld [wBattleMonHP + 1], a
ld [wHPBarNewHP], a
ld b, [hl]
ld a, [wBattleMonHP]
ld [wHPBarOldHP+1], a
sbc b
ld [wBattleMonHP], a
ld [wHPBarNewHP+1], a
So if I'm right (and this code is working properly, which I don't know - it's Gen 1), our most significant byte for battle HP is at wBattleMonHP
, and the least significant byte at wBattleMonHP + 1
.
The .healEffect
code compares current HP to max HP in the other order - it cp
s wBattleMonHP
first, then "borrows" any necessary carry bit from wBattleMonHP + 1
.
That would mean that when we need to carry, we subtract one more from the least significant byte with sbc
, and then compare to zero. So if the subtraction would have been 1 (e.g. the difference is 255 or "-1"), it becomes zero and the heal is .failed
And interesting consequence if I'm understanding correctly, is that there's an edge case when you don't have to carry, where healing works as expected:
Max HP: 00000001 11111111 # 511
Current HP: 00000001 00000000 # 256
cp "first" byte results in no carry (1-1 is possible without borrowing)
sbc "second" byte is 0-255 = 1 and borrow
But 1 is not 0, so we heal even though the delta is 255!
I kind of want to test this now, if I can find my old pokemon game...
I tested this in Pokemon Red and it works! A level 80 Chansey with 511 max HP and 256 current HP does heal with Softboiled, even though the difference in HP is 255. Since the current HP has the same most significant byte (0x01) as its max HP, there is no carry flag from the first cp
, and the subtraction correctly identifies that the hp is not equal to the max hp.