-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Implementing Catch EXP
Beginning with Pokémon X and Y, you could earn EXP by catching a Wild Pokémon. Wouldn't it be nice to backport this feature to Pokémon Emerald? Well, it's relatively easy.
This tutorial has two parts. The "Figuring it out" section walks you through the process of figuring out how to implement Catch EXP from scratch: looking at game systems, understanding them, making code changes, testing them, and fixing bugs as they come up. The "Just the code changes" section lists only the three relatively small code changes you need to make in order to get a bug-free Catch EXP feature.
The "Figuring it out" section is a much, much longer read; this is because I try to walk you step by step through how to implement something like this, including finding functions we need to edit and reasoning about what could be causing some of the bugs we run into when setting this up from scratch. If you're not used to making these kinds of edits, or if you struggle with troubleshooting bugs, I think it might be a good idea to tough it out, read it all, and see if it can teach you anything useful.
A helpful thing to know is that Game Freak actually built an entire scripting language for the battle system, a lot like the scripting language used for overworld events, and they scripted a lot more than you'd expect. All of the move effects are scripted, but so are things like most of the process of catching a Wild Pokémon. The game bounces back and forth between hardcoded C and scripts like a ping-pong ball in order to accomplish what Game Freak wanted. This may sound disorganized (and it kind of is!), but it means that Game Freak's designers could fine-tune basic behaviors of the battle system without having to edit the deeper engine. For example, when you catch a Wild Pokémon, what should happen first? Should you get a chance to rename it, or should you see its Pokédex entry? Each of these is a script command, so the core functionality is in the engine's C code, but the script decides when -- and in what order -- to activate it.
We can find the scripts for Poké Balls' battle effects in data/battle_scripts_2.s
. Let's look at the script for a successful catch:
BattleScript_SuccessBallThrow::
jumpifhalfword CMP_EQUAL, gLastUsedItem, ITEM_SAFARI_BALL, BattleScript_PrintCaughtMonInfo
incrementgamestat GAME_STAT_POKEMON_CAPTURES
BattleScript_PrintCaughtMonInfo::
printstring STRINGID_GOTCHAPKMNCAUGHTPLAYER
trysetcaughtmondexflags BattleScript_TryNicknameCaughtMon
printstring STRINGID_PKMNDATAADDEDTODEX
waitstate
setbyte gBattleCommunication, 0
displaydexinfo
BattleScript_TryNicknameCaughtMon::
printstring STRINGID_GIVENICKNAMECAPTURED
waitstate
setbyte gBattleCommunication, 0
trygivecaughtmonnick BattleScript_GiveCaughtMonEnd
givecaughtmon
printfromtable gCaughtMonStringIds
waitmessage B_WAIT_TIME_LONG
goto BattleScript_SuccessBallThrowEnd
BattleScript_GiveCaughtMonEnd::
givecaughtmon
BattleScript_SuccessBallThrowEnd::
setbyte gBattleOutcome, B_OUTCOME_CAUGHT
finishturn
That's a lot, so let's break it down. The lines that aren't indented, and that end with two colons, are labels. That is, they define named locations for code. We can reference them from C code (for example, the hardcoded game engine can decide to run the script code starting at BattleScript_SuccessBallThrow
), and we can also "jump" to them if certain conditions are met. Let's look at just the first few lines of the script to get a handle on this idea:
BattleScript_SuccessBallThrow::
jumpifhalfword CMP_EQUAL, gLastUsedItem, ITEM_SAFARI_BALL, BattleScript_PrintCaughtMonInfo
incrementgamestat GAME_STAT_POKEMON_CAPTURES
BattleScript_PrintCaughtMonInfo::
So that first command roughly translates to: "We want to jump to another spot if a two-byte value (a 'halfword') is equal to some other value. The values we want to compare are the value stored in the gLastUsedItem
variable, and ITEM_SAFARI_BALL
. If they're equal, we want to jump to BattleScript_PrintCaughtMonInfo
." You can see that our jump target, the "print caught 'mon info" label, is just two lines down, so if we "take the jump," we'll be skipping the next line, the next command.
The command that we're potentially skipping roughly translates to, "We keep track of how many Pokémon the player has caught. Please increase that game stat by one." So what we're doing is, if the player used a Safari Ball, then we skip increasing the player's "Pokémon captures" counter; but if the player used any other ball, then we increase that count. We only want to keep track of normal captures under normal circumstances, you see; the Safari Zone is so different that we don't even want to count it.
Let's keep reading onward.
BattleScript_PrintCaughtMonInfo::
printstring STRINGID_GOTCHAPKMNCAUGHTPLAYER
trysetcaughtmondexflags BattleScript_TryNicknameCaughtMon
printstring STRINGID_PKMNDATAADDEDTODEX
waitstate
setbyte gBattleCommunication, 0
displaydexinfo
BattleScript_TryNicknameCaughtMon::
So we print some text to tell the player that they successfully caught the Pokémon. Then, we try and set the caught Pokémon's "owned" flag in the Pokédex. If that fails (because the flag is already set), then we skip ahead to BattleScript_TryNicknameCaughtMon
. The code that we might end up skipping is responsible for a few things, but mainly, it displays the caught Pokémon's Pokédex data; this would be the first time the player sees that information. (I promise that me showing you this will turn out to be relevant.)
You'll note that the trysetcaughtmondexflags
command and the jumpifhalfword
command both perform conditional jumps, but they look pretty different. There isn't really a consistent convention in how these script commands "look," so don't get too hung up on trying to spot a pattern.
Remember two sections ago when I said that a helpful thing to know is that Game Freak actually built an entire scripting language for the battle system, a lot like the scripting language used for overworld events, and they scripted a lot more than you'd expect?
A helpful thing to know is that Game Freak actually built an entire scripting language for the battle system, a lot like the scripting language used for overworld events, and they scripted a lot more than you'd expect. All of the move effects are scripted, but so are things like most of the process of catching a Wild Pokémon.
Well, it turns out, there's a script command for awarding EXP, too, because winning a battle is also a script. If you go to data/battle_scripts_1.s
and Ctrl + F for xp
, you'll find a lot of stuff related to the move Explosion, but you'll eventually find this:
BattleScript_GiveExp::
setbyte sGIVEEXP_STATE, 0
getexp BS_TARGET
end2
So there's a script command named getexp
, and it takes a single parameter, which seems to be the Pokémon whose defeat is the source of that EXP. If we search for all uses of the getexp
command, we'll find that before we run the command, we always run setbyte sGIVEEXP_STATE, 0
, too. So that sounds simple enough: let's just add that to the catch script.
We can also add some code comments by prefixing them with the @
symbol: these *.s
files are assembly files, and when the assembler (like a compiler, but not) sees a @
, it ignores all other text until the end of the line. We can use that to annotate our changes with some explanations.
BattleScript_SuccessBallThrow::
jumpifhalfword CMP_EQUAL, gLastUsedItem, ITEM_SAFARI_BALL, BattleScript_PrintCaughtMonInfo
incrementgamestat GAME_STAT_POKEMON_CAPTURES
BattleScript_PrintCaughtMonInfo::
printstring STRINGID_GOTCHAPKMNCAUGHTPLAYER
+ @
+ @ ROM hack edit: give catch EXP:
+ @
+ setbyte sGIVEEXP_STATE, 0
+ getexp BS_TARGET
+ @
trysetcaughtmondexflags BattleScript_TryNicknameCaughtMon
Compile the ROM as usual, and then start up Emerald. If you have an existing save file (and haven't made any changes to savegame data since your last compile), then you can use that. Otherwise, play up until you have some Poké Balls, and then park by some tall grass and save the game. We're not going to change how the savegame is formatted during this tutorial, so you should be able to reuse this save file for all of the tests we're going to run. (Spoiler: There'll be a few.)
If you enter a wild battle and catch a new Pokémon, you should see this:
Catch.EXP.Tutorial.-.Test.1.mp4
That... mostly works! There's some jank to it, though. The music for a successful capture starts to play, but then it gets cut off by the music for winning a wild battle. Why does that happen? Well, the only thing we added was code to grant some EXP, so I guess we need to look at how the getexp
command actually works.
Let's head to src/battle_script_commands.c
. Each of the C functions that defines a command is named after the command itself, so if we Ctrl + F for getexp
, we'll find the function we want: Cmd_getexp
. Beginning at line 3241 and ending at line 3518, this is a bit of a big one! So before we continue, let's talk about how battle scripts actually work.
-
Under the hood, a battle script is just a blob of bytes. The battle engine is told to read bytes at a given location, so it sets the variable
gBattlescriptCurrInstr
("battle script current instruction") to refer to that location. Then it reads the first byte it sees there and goes, "Ah! This must be a command ID." It looks up the n-th command in its dictionary of script commands and runs that command's C function. -
A typical script command will begin by reading additional bytes starting at (
gBattlescriptCurrInstr + 1
), to retrieve whatever parameters it uses. For example,jumpifhalfword
would read the byte at (gBattlescriptCurrInstr + 1
) to get the kind of comparison we want to run (e.g.CMP_EQUAL
to check if two values are equal). -
The script command would then increase
gBattlescriptCurrInstr
. If you think ofgBattlescriptCurrInstr
as an arrow pointing at the spot we want to read from, then "increasing" it just moves the arrow forward. Now, bear in mind that we didn't increasegBattlescriptCurrInstr
before running the script command; so, when the script command ran,gBattlescriptCurrInstr
was the location of the command itself, not the options that the script wanted to give it.
We need to understand this stuff because getexp
... doesn't work like that.
The getexp
command has to do a lot of calculations, and Game Freak didn't want to lag the game, so they decided to split those calculations into six batches. The getexp
command relies on a counter in order to know what batch it needs to run next. The counter is called gBattleScripting.getexpState
in C, and sGIVEEXP_STATE
in scripts: that's why we always have to run a setbyte sGIVEEXP_STATE, 0
script command before we run getexp
.
Here's the clever trick: getexp
doesn't increase gBattlescriptCurrInstr
when it finishes, unless it's finished the sixth batch. So what ends up happening before that batch?
- The script sets
gBattleScripting.getexpState
to 0. - The script runs
getexp
, sogBattlescriptCurrInstr
points to thegetexp
instruction in the script. -
Cmd_getexp
sees thatgBattleScripting.getexpState
is 0, so it runs the first batch (computers count from 0, not 1). -
Cmd_getexp
finishes running and increasesgBattleScripting.getexpState
to 1 on its way out. - The battle script engine wants to run the next command, which is the command at
gBattlescriptCurrInstr
... except that we didn't increasegBattlescriptCurrInstr
, didn't move it forward, so it still points to thegetexp
instruction. The one we just ran. - ...So the script runs
getexp
again. -
Cmd_getexp
sees thatgBattleScripting.getexpState
is now 1, so it runs the second batch. -
Cmd_getexp
finishes running and increasesgBattleScripting.getexpState
to 2 on its way out. - The battle script engine wants to run the next command. We still haven't increased
gBattlescriptCurrInstr
, so... - ...we run the same
getexp
instruction yet again. -
Cmd_getexp
sees thatgBattleScripting.getexpState
is now 2, so it runs the third batch. -
Cmd_getexp
finishes running and increasesgBattleScripting.getexpState
to 3 on its way out. - The battle script engine wants to run the next command. We still haven't increased
gBattlescriptCurrInstr
. Do you see what we're doing here? - We run the same
getexp
instruction again. -
Cmd_getexp
sees thatgBattleScripting.getexpState
is now 3, so it runs the fourth batch. -
Cmd_getexp
finishes running and increasesgBattleScripting.getexpState
to 4 on its way out. - The battle script engine wants to run the "next" (haha) command.
- We run the same
getexp
instruction again. -
Cmd_getexp
sees thatgBattleScripting.getexpState
is now 4, so it runs the fifth batch. -
Cmd_getexp
finishes running and increasesgBattleScripting.getexpState
to 5 on its way out. - The battle script engine wants to run the "next" command.
- We run the same
getexp
instruction again. -
Cmd_getexp
sees thatgBattleScripting.getexpState
is now 5, so it runs the last batch... and as part of that batch, it finally increasesgBattlescriptCurrInstr
. -
Cmd_getexp
finishes running. We're finally free. - The battle script engine wants to run the next command. Finally, it sees the command after
getexp
, which for our catch script istrysetcaughtmondexflags
.
So now that we know how getexp
is structured, it'll be easier to read it. All the code is broken into a big switch
statement that divides the code into these six batches, but since we know they're all going to run sequentially anyway (just not on the same frame), we can pretty much just ignore the switch
and mentally read the whole Cmd_getexp
function as one big chunk of code.
Try reading the code for getexp
yourself. You don't have to understand it all; just skim it until you see something that looks related to music. The music IDs in this game have names that start with MUS_
. Come back here when you spot it, or if you can't find it.
If you looked over the code and didn't spot what we're looking for, I'll save you some time: it's in the third batch, or case 2
. Beginning at line 3345:
// music change in wild battle after fainting a poke
if (!(gBattleTypeFlags & BATTLE_TYPE_TRAINER) && gBattleMons[0].hp != 0 && !gBattleStruct->wildVictorySong)
{
BattleStopLowHpSound();
PlayBGM(MUS_VICTORY_WILD);
gBattleStruct->wildVictorySong++;
}
Ah! They want the victory theme to start playing when you're told how much EXP you're given, so they just... jammed the code for it right into getexp
. After all, the only way to gain EXP during a wild battle is to faint the one (1) wild Pokémon you're battling. There's no other way to gain EXP in a wild battle. Nope. Can't happen.
Okay, so how do we fix it? We need to make the game double-check that the battle isn't ending with a capture, but how can we know, at this point in the code, whether the wild Pokémon has been caught?
Well, what does catching a wild Pokémon actually do? Is there something that we can detect? Some variable being set somewhere, perhaps?
Let's look back at the script for successfully catching a wild Pokémon:
BattleScript_SuccessBallThrow::
jumpifhalfword CMP_EQUAL, gLastUsedItem, ITEM_SAFARI_BALL, BattleScript_PrintCaughtMonInfo
incrementgamestat GAME_STAT_POKEMON_CAPTURES
BattleScript_PrintCaughtMonInfo::
printstring STRINGID_GOTCHAPKMNCAUGHTPLAYER
That's everything that happens before we try to getexp
, and hm, nope, there's nothing in there that we can check for. Okay, but how do we actually get to the BattleScript_SuccessBallThrow
label? If we try to Ctrl + F the battle script files, we won't find any other uses of the label name.
Well, here's the trick. Remember when I showed you the catch script, and I said that the lines that aren't indented and that end with two colons are labels that define named locations for code, and we can reference them from C code, and we can also "jump" to them if certain conditions are met?
The lines that aren't indented, and that end with two colons, are labels. That is, they define named locations for code. We can reference them from C code (for example, the hardcoded game engine can decide to run the script code starting at
BattleScript_SuccessBallThrow
), and we can also "jump" to them if certain conditions are met.
Here's where that comes into play. When the game decides that you've successfully caught a Pokémon, it sets the gBattlescriptCurrInstr
"arrow" to point to BattleScript_SuccessBallThrow
. Everything in battles is scripted except when it isn't, so let's Ctrl + F inside of src/battle_script_commands.c
for BattleScript_SuccessBallThrow
.
That takes us to line 9938, which is in the middle of Cmd_handleballthrow
. There are actually two different places where we use C to have the script jump to BattleScript_SuccessBallThrow
-- and an important thing to understand is that from the script's perspective, the jump is instant, but from C's perspective, we only jump when it's time to run the next script instruction; the rest of Cmd_handleballthrow
still runs. So what does it do?
SetMonData(&gEnemyParty[gBattlerPartyIndexes[gBattlerTarget]], MON_DATA_POKEBALL, &gLastUsedItem);
It doesn't set any variables or do anything else to "remember," within C, that a successful capture has occurred, because it doesn't really need to remember that; we've decided that the next script instruction to run should be the "successful capture" script, and that'll do whatever it needs to do. What does happen in C, though, is this one line of code that we can take advantage of. When you catch a Pokémon, the game modifies that Pokémon to store the type of Poké Ball you used, so that you can see that ball type when viewing the Pokémon's stats.
We set that information. We can get that information.
You can look up the GetMonData
and SetMonData
functions in include/pokemon.h
to see how they work, but I'll save you the trouble for now. Here's how we want to change getexp
:
// music change in wild battle after fainting a poke
if (!(gBattleTypeFlags & BATTLE_TYPE_TRAINER) && gBattleMons[0].hp != 0 && !gBattleStruct->wildVictorySong)
{
+ if (GetMonData(&gEnemyParty[gBattlerPartyIndexes[gBattlerTarget]], MON_DATA_POKEBALL) == ITEM_NONE) {
BattleStopLowHpSound();
PlayBGM(MUS_VICTORY_WILD);
gBattleStruct->wildVictorySong++;
+ }
}
If the wild Pokémon doesn't have a Poké Ball type set, then it must not have been captured. The only way we can reach getexp
during a wild battle is if the Pokémon is captured or defeated, so it must have been defeated, so now we can let the victory music play.
Let's try in-game and see how it works.
Catch.EXP.Tutorial.-.Test.2.mp4
The music issue is solved! However, there's another subtle problem that we see when I capture Wurmple. If we level up from catch EXP, and then are shown the caught Pokémon's Pokédex entry, then all the text is shifted to the side, and it wraps around to the other end of the screen. That's... Well, that's not great.
Okay, this one would actually be a lot harder to find, because it would require that you dig through the Pokédex user interface, which is one of the most complex UIs in the entire game, so I'm going to give you the short version.
- UIs in this game define windows, which reserve portions of the GBA's VRAM for graphics data.
- Game Freak has a text printer system that is designed to draw text onto those windows.
- The Game Boy Advance divides graphics into four "background" ("BG") layers that use 8-by-8-pixel tiles, and an "object" ("OBJ") layer for sprites.
- Windows only use background tiles, and each window can only exist on a single layer.
If you're testing with mGBA, then you should be able to go to the Tools menu, navigate to Game state views, and choose to View map. This will let us look at each of the background layers. We can see that the Pokédex text is all on layer 2, and the visual parts of the UI (minus Wurmple's sprite) are on layer 3.
Hm, nope, that actually looks normal. There's no shifting within the background layer itself; the text printer is doing its job properly. The GBA can be told to shift an entire BG layer around on the screen, and when it does so, they wraparound. Checking for that is a little advanced, but we can do it. Go to Tools, navigate to Game state views, and select View I/O registers. The I/O registers are just special variables that a ROM's code can use to talk directly to the GBA's hardware. Each register has a special purpose and its value has a special meaning.
Let's select register 0x4000018: BG2HOFS
: "background 2 horizontal offset."
According to this, there's a horizontal offset of 416 pixels. Let's try unchecking all of the checkboxes and clicking "apply," to force the offset to zero, and see if that fixes anything, and... nothing happened! If we switch to any other I/O register in mGBA and then switch back to BG2HOFS
, then it shows 416 again. It's like we never even changed the value. Is the game somehow re-shifting the background every frame, as if it just really doesn't want us messing with it? Where could the code be doing that?
Well, this problem we're having -- it only happened when we leveled up, right? How does that work?
How does leveling up...
...in battle...
It's a script command. Battles use tons of scripting; the script commands are always a good first place to look for things.
The BattleScript_LevelUp
script in data/battle_scripts_1.s
calls the drawlvlupbox
function. The Cmd_drawlvlupbox
function is very similar to getexp
: it's another one of those commands that runs itself over and over in order to divide its work up across multiple frames. We can see that it sets a bunch of variables that look like gBattle_BG2_Y
. I guess the battle engine checks these variables on every frame and uses them to decide where the four BG layers should be shifted to. It's probably meant as a way to let move animations easily manipulate graphics on the BG layers.
Why is drawlvlupbox
shifting a BG layer, though, I wonder? Well, PRET left this handy-dandy code comment in Cmd_drawlvlupbox
:
// If the Pokémon getting exp is not in-battle then
// slide out a banner with their name and icon on it.
// Otherwise skip ahead.
if (IsMonGettingExpSentOut())
gBattleScripting.drawlvlupboxState = 3;
else
gBattleScripting.drawlvlupboxState = 1;
If a Pokémon gains EXP when it's not on the field, the game displays a cute little banner showing its icon and name... and it uses a sliding animation! We only run case 1
and case 2
in Cmd_drawlvlupbox
if we want to draw that banner. Case 1 calls the InitLevelUpBanner
function, and if we Ctrl + F that,...
static void InitLevelUpBanner(void)
{
gBattle_BG2_Y = 0;
gBattle_BG2_X = LEVEL_UP_BANNER_START;
Aha! They're placing that banner on BG layer 2, so they want to shift the layer for it. Keep running Ctrl + F for LEVEL_UP_BANNER_START
and you'll find that it's a constant set to 416, the exact amount by which our background was shifted! You may also find the function that we actually hit in our test: SlideOutLevelUpBanner
, which shifts BG layer 2 back to LEVEL_UP_BANNER_START
even if we never slid the level-up banner in in the first place.
If we go back to our "successful catch" script and read it, one command should jump out at us: displaydexinfo
. I bet the C code for that will be named Cmd_displaydexinfo
. Let's find it.
Looking at the code, we can see that this is yet another script command that runs itself over and over. This time, though, it's because it needs to wait for you to click through the Pokédex entry before it allows the script to continue. We can see that in case 1
, it calls a function named DisplayCaughtMonDexPage
, so let's reset the battle-specific "BG layer 2" variables before we call that. In fact, let's reset all of the layer offsets just to be real sure. When we looked at the Pokédex screen's BG layers in mGBA, we saw that they're all set up so that they'd only display properly if they're not shifted. We've seen gBattle_BG2_X
, we know there are four BG layers, and we know that computers count from 0 rather than 1, so we can guess the other variable names easily enough (and we can double-check them with Ctrl + F to make sure).
case 1:
if (!gPaletteFade.active)
{
FreeAllWindowBuffers();
+ gBattle_BG0_X = 0;
+ gBattle_BG0_Y = 0;
+ gBattle_BG1_X = 0;
+ gBattle_BG1_Y = 0;
+ gBattle_BG2_X = 0;
+ gBattle_BG2_Y = 0;
+ gBattle_BG3_X = 0;
+ gBattle_BG3_Y = 0;
gBattleCommunication[TASK_ID] = DisplayCaughtMonDexPage(SpeciesToNationalPokedexNum(species),
gBattleMons[gBattlerTarget].otId,
gBattleMons[gBattlerTarget].personality);
gBattleCommunication[0]++;
}
break;
Let's give it another try.
Catch.EXP.Tutorial.-.Test.3.mp4
It works! The music is correct, and we don't glitch out the Pokédex text! We've implemented catch EXP.
It took some digging, but we didn't actually have to change a lot of code. Let's recap...
BattleScript_SuccessBallThrow::
jumpifhalfword CMP_EQUAL, gLastUsedItem, ITEM_SAFARI_BALL, BattleScript_PrintCaughtMonInfo
incrementgamestat GAME_STAT_POKEMON_CAPTURES
BattleScript_PrintCaughtMonInfo::
printstring STRINGID_GOTCHAPKMNCAUGHTPLAYER
+ @
+ @ ROM hack edit: give catch EXP:
+ @
+ setbyte sGIVEEXP_STATE, 0
+ getexp BS_TARGET
+ @
trysetcaughtmondexflags BattleScript_TryNicknameCaughtMon
In Cmd_getexp
in src/battle_script_commands.c
:
// music change in wild battle after fainting a poke
if (!(gBattleTypeFlags & BATTLE_TYPE_TRAINER) && gBattleMons[0].hp != 0 && !gBattleStruct->wildVictorySong)
{
+ if (GetMonData(&gEnemyParty[gBattlerPartyIndexes[gBattlerTarget]], MON_DATA_POKEBALL) == ITEM_NONE) {
BattleStopLowHpSound();
PlayBGM(MUS_VICTORY_WILD);
gBattleStruct->wildVictorySong++;
+ }
}
In Cmd_displaydexinfo
in src/battle_script_commands.c
:
case 1:
if (!gPaletteFade.active)
{
FreeAllWindowBuffers();
+ gBattle_BG0_X = 0;
+ gBattle_BG0_Y = 0;
+ gBattle_BG1_X = 0;
+ gBattle_BG1_Y = 0;
+ gBattle_BG2_X = 0;
+ gBattle_BG2_Y = 0;
+ gBattle_BG3_X = 0;
+ gBattle_BG3_Y = 0;
gBattleCommunication[TASK_ID] = DisplayCaughtMonDexPage(SpeciesToNationalPokedexNum(species),
gBattleMons[gBattlerTarget].otId,
gBattleMons[gBattlerTarget].personality);
gBattleCommunication[0]++;
}
break;