Generating Saves for Pokémon Red/Blue

Generating Saves for Pokémon Red/Blue

This article chronicles the development of pokegen - a web service to generate Pokémon save file - allowing you to load in at any state. As of writing, the application is in a minimal state. See commit 50029d4.

Motivation

My GitHub profile feels empty. I want to create a repository as a technical resume. Something that I can slowly stuff with absolutely everything I've learnt - and will learn.

I am inspired by LiveOverflow's video Dissecting Pokmon Red Savegame. It's a great watch, and super relatable for anyone who played Pokemon games as a kid. Most of LiveOverflow's videos go straight over my head, but this one I could just about follow.

What can pokegen do?

Right now, can use `curl` to create a file with a player name and rival name.

curl localhost:8080/gen \
-d '{"player_name":"Red", "rival_name":"Gary"}' \
--output "Pokemon Red.sav"        
Player in their bedroom with the menu open, the menu lists player name "Red"

Using the mGBA emulator and a ROM ??? of Pokémon Red or Blue, place the .sav file next to your ROM ??.Ensure they have the same name - ignoring the extension Load up the game with mGBA. After the intro plays, choose to Continue your adventure. This will load up the .sav file and wham! You're at the start of the game. Your character will have whatever name you specified - in this case "Red".

A little later on you'll be introduced to your rival "Gary" by Professor Oak. Unfortunately, you cannot name the professor ??.

How does this look under the hood?

The .sav file is about 32kb in size, so there's a lot of pieces we could talk about. The naming is relatively simple. I will refer to Bulbapedia for a lot of this.

The player name is located at offset 0x2598. It always uses 11 bytes. Although the name you choose will likely be shorter than that, so a terminator byte (0x50) is used to know how much of the 11 bytes you used. Including the terminator byte, that means you only really have 10 bytes to write your name.

[By the way, in-game you're only allowed to enter 7 characters for your name. I wonder what will happen if you bypassed that with this tool ??]

Here are the bytes for "player_name", generated by the `curl` command earlier:

91A4A350 00000000 000000        

The first 4 bytes are the most interesting, as everything after the terminator (0x50) is just to meet the 11 byte requirement.

The bytes 0x91 0xA4 0xA3 spell out "Red". You may have noticed this does not match-up with Unicode (w/ UTF-8). Pokémon Gen 1 uses a different character-set. That means pokegen must convert over your Unicode (w/ UTF-8) input into Pokémon Gen 1 output. I can already feel the number of edge cases I haven't discovered with this feature.

If you're curious how I handled user input (such as player name), see the implementation ??

The checksum

Every Pokémon save file is validated with checksums. To ensure the integrity of the save file (and maybe from people messing with it ??), the developers introduced a few checksums in sections of the save file. The game will complain when the data doesn't match the checksum. It considers the file "destroyed" ??.

No alt text provided for this image

Information, like the player name, will affect the main data checksum. The main data checksum is a single byte located at offset 0x3523. We will need to calculate the checksum and write it at the end. You can read how the checksum is calculated if you're interested.

Calculating ?? the checksum could be done at the very end, by going over the bytes a second time. I prefer to calculate it simultaneously as writing the main data. Here's how I did that...

A nice feature of Go is implicit interfaces - such as the io.Writer ??. Most of the functions in the pokegen codebase accept io.Writer (including the character-set implementation earlier). That's a nice abstraction because the functions do not care where the bytes are written to. You can also wrap around a writer to sneak in ?? some extra behaviour. For calculating the checksum, I can wrap the base io.Writer with my own io.Writer implementation - in this case checksumWriter. I can then insert my own logic before doing the real writing. In the case of checksumWriter, I'm changing the value of a new, internal `sum` field.

type checksumWriter struct {
	w ? io.Writer
	sum byte
}

func (csw *checksumWriter) Write(bytes []byte) (int, error) {
	for _, b := range bytes {
		csw.sum += b
	}
	return csw.w.Write(bytes)
}

func (csw checksumWriter) WriteChecksum() (int, error) {
   return csw.w.Write([]byte{^csw.sum})
}        

Now, I can swap out the original writer with this custom writer. All the functions that accept io.Writer are now talking to my new io.Writer, but are none-the-wiser. My writer is still doing the original's work, but it's also calculating the eventual checksum on every call. This lets me simply return the pre-calculated checksum once we're done writing the main data.

...
_, err = csw.WriteChecksum()
if err != nil {
   return fmt.Errorf("failed to write checksum: %w", err)
}
...        

That's pretty much it. It's not very exciting, but I hope to add more and update this blog as we go. I will write partly about the file format itself and partly about the implementation details. So here's what's coming up...

What is To Do?

There's so much left to do. Here are a few things I want to setup...

  • ?? Tests! It's such a pain to manually load up the emulator to check for breakages
  • ?? More fields! There are so many things to play with, like Money, Location and Pokémon Team
  • ?? Deployment! I want to run this somewhere

要查看或添加评论,请登录

社区洞察

其他会员也浏览了