Part 7: The Main Event

The Generation 3 Pokémon games include a (semi) new feature known as Mystery Event in Ruby and Sapphire, and Mystery Gift in FireRed, LeafGreen, and Emerald. This feature allowed GameFreak to create small events that could be added to the games through various methods, allowing players to encounter exclusive new Pokémon and obtain exclusive new items. These events would continue in later games, but they would be hardcoded at launch. What is unique about events in Generation 3 is that the whole event is stored within the save file, written in Pokémon's custom script. This custom scripting language has been reverse-engineered by the Pokémon community, and programs such as HexManiacAdvance by haven1433 have been created to simplify writing in this niche language. These events can be custom-made as well, and I first learned about this from this video by im a blisy. As soon as I saw this being done, I knew I had to implement it into my program.

The event is stored in the save file in a location known as "RAM Script". All five games store the RAM Script in the same save data section, #2- but the location within this section is different between Ruby/Sapphire, FireRed/LeafGreen, and Emerald. Thankfully, we can determine this offset thanks to the decomplications created by the PRET team; pokeruby, pokefirered, and pokeemerald. Ruby and Sapphire are simple enough, as their offset is static. However, as another anti-cheating measure, much of the loaded save data is shifted within FireRed/LeafGreen and Emerald. The offset of this data is stored in a known location though, so ultimately this doesn’t hinder us too much.

Creating the Event

The next step is to actually begin writing the event itself. Each event is stored as a set of 1004 bytes: a 2-byte long checksum, 2 bytes of padding, and then 1000 bytes of space for the script. Our script needs to do the following things:

  • Tell the game which NPC should activate the script

  • Check flags in the save data

  • Print text

  • Set, add, and subtract variables

  • Send Pokémon to the PC

  • Set Pokémon as Caught in the PokéDex

  • Run conditionals

  • Play music

Most of these functions are part of the custom script language- but not all. Sending Pokémon to the PC and setting them as Caught in the PokéDex are not. These functions do exist in the game's ROM though, and we can access them through a special command called CallASM.

Assembly Code

CallASM is by far the most powerful command that we have access to. With it, we can jump directly to most locations within the RAM and ROM, and interpret the bytes as ARM7 Thumb code. This means that we can do basically anything we want! The only downside is… it's ARM7 Thumb code; it isn't the most straightforward language to write in. The goal is simple: run the CopyMonToPC (originally called SendMonToPC) and GetSetPokedexFlag functions, using CallASM. The execution, however, is much easier said than done. These function locations can be found within the .sym file in each of the PRET decompilizations. Emerald's can be found here for example.

Our first challenge is that our event script isn't always within the same location in memory. In FireRed, LeafGreen, and Emerald the event script is shifted between 0 and 255 bytes in memory- likely to discourage doing exactly what we're trying to do. Fortunately, this offset is stored within memory as SaveBlock1PTR. By taking this offset and adding it to the standard script offset, we can know exactly where our script is stored in memory. This means that we can jump to any custom ASM we write.

We have two functions that we want to call- CopyMonToPC and GetSetPokedexFlag. Let's take a look at the implementation of CopyMonToPC in Emerald's decompilation:

The CopyMonToPC function takes in a pointer to the data structure of the Pokémon it should copy to the PC, and returns a value of either MON_GIVEN_TO_PC (1) or MON_CANT_GIVE (2). In order to run this function successfully through CallASM, we need to set register 0 to the pointer where our Pokémon data structure is. Fortunately, we can determine that the same way we determined where our ASM code was located. We also can use ASM code to set the returned value to one of the variables accessible from the event script.

Now lets take a look at GetSetPokedexFlag:

In GetSetPokedexFlag's case, we have two variables that need to be passed in, and one variable that is returned. However, we don't need to do anything with the returned value, so it can be ignored. The first value is the National PokéDex number for the Pokémon we want to set as caught. The second value is the caseID, which will be either FLAG_SET_SEEN or FLAG_SET_CAUGHT. Pokémon need to be set as Seen in order to be set as Caught, so we will need to run the function twice- once for Seen and once for Caught. We can pass these variables into registers 0 and 1 respectively, and run our ASM with no issues!

There's one more challenge left to tackle- not every version of each game is the same. Ruby and Sapphire have three versions (1.0, 1.1, and 1.2), while FireRed and LeafGreen have two (1.0 and 1.1). Emerald only has one. Each one of these versions have different locations for each function within the ROM, so we need to have all of them stored and ready to go, depending on what game is currently inserted. On top of this, each of the six different languages also has differing locations. This leads to a grand total of 48 different ROMs we need to account for, since not every version was released in every language. My solution for this was to effectively implement a custom ASM compiler in my program, and store all of the relevant data locations for each ROM and Language. That way, I could easily modify the script as I needed to, and it would be compatible with all 48 versions.

This custom compiler is one of the things I am most proud of, and has made bug testing and script modifications so much simpler. The entire file is too large to put here, but I would highly recommend checking it out on my GitHub, here!

Previous
Previous

Part 6: Texts and Dexes

Next
Next

Part 8: A Day-Long Detour