Saving data on a Controller Pak
Libdragon has support for storing save data on the N64’s Controller Pak. This was generally done is commercially produced games to either store massive save files or because they wanted to cut costs by excluding an on-cartridge EEPROM chip (Or SRAM or Flash). Let’s have a look at how this can be done.
The controller pak can hold 128 blocks, each being 256 bytes in size, that’s 32kb in total. There also can only be 16 different save entries in one controller pak.
Initialising, identifying and validating the Controller Pak
The initialisation function for the Controller Pak system is included within the same controller_init()
that is used by the controller subsystem, so there is no need to include anything new.
However, it is important to check whether there is a Controller Pak inserted and ready in order to be able to work with it.
// Function prototype
int identify_accessory (int controller);
// Example to check if there is a controller pak in port #1
int accessory_0 = identify_accessory(0);
if (accessory_0 == ACCESSORY_MEMPAK) {
// Good to go! Let's validate it
if (validate_mempak(0) == 0) {
// Controller pak is valid, let's perform some action
} else if (validate_mempak(0) == -2) {
// Controller pak not found
} else {
// Controller pak is found, but corrupted or unformatted
}
} else {
// Controller pak not found
}
Controller Pak entries
Since the Controller Pak can contain save data from many different games at the same time, it’s important to understand how Libdragon interacts with all the different saves within it.
Data structure
An entry is a data structure that holds the save data plus some metadata.
typedef struct entry_structure {
// Vendor ID
uint32_t vendor;
// Game ID
uint16_t game_id;
// Inode pointer
uint16_t inode;
// Intended region
uint8_t region;
// Number of blocks used by this entry.
uint8_t blocks;
// Validity of this entry.
uint8_t valid;
// ID of this entry
uint8_t entry_id;
// Name of this entry
char name[19];
} entry_structure_t;
Searching for data entries
Each Controller Pak can only hold 16 of these so if we’re searching for an entry that holds data for our game within all possible Controller Paks, we need to:
- Loop through all controllers and check which one has a Controller Pak inserted.
- For the ones that do have a Controller Pak, validate it
- The Controller Pak is valid, so loop through 0 to 15
- Check if the entry is valid
- Check if the game’s ID or name matches ours
- Now we have found a controller pak entry for our game and can manipulate it.
It is also possible to read all the data from the Controller Pak, but it’s overkill and doesn’t really let us distinguish useful info from blank data or other game’s save data.
Here’s an example of how to find them. It will find every instance of a save file in all the Controller Paks inserted.
// Loop through controllers
for(int i = 0; i < 4; i++) {
// Check if this controller has an accessory
if (get_accessories_present() & (0xF << i)) {
// Check if it is a Controller Pak
if (identify_accessory(i) == ACCESSORY_MEMPAK) {
// Validate the Controller Pak
if (validate_mempak() == 0) {
// Loop through all possible entries in the controller pak
for (int j=0; j<16; j++) {
// Load the entry into RAM
entry_structure_t entry;
get_mempak_entry(i, j, &entry);
// Check if the entry is valid
if (entry.valid) {
// Check if it is our game by name
if (strcmp(entry.name, "MYGAME") == 0) {
// Found it!
// Let's do some stuff
}
}
}
}
}
}
}
Creating/deleting entries
Creating a new entry follows a very similar process to searching for them. The only difference is instead of looking for valid entries, we’re looking for invalid entries. Once we find an invalid entry, we can set its name, block count, region and write data to it.
for(int i = 0; i < 4; i++) {
if (get_accessories_present() & (0xF << i)) {
if (identify_accessory(i) == ACCESSORY_MEMPAK) {
if (validate_mempak() == 0) {
for (int j=0; j<16; j++) {
entry_structure_t entry;
get_mempak_entry(i, j, &entry);
// Check if the entry is valid
if( !entry.valid ) {
// Not valid! We can write to it now
// Create a buffer filled with zeros
uint8_t *data = calloc(MEMPAK_BLOCK_SIZE, sizeof(uint8_t));
// Set the save file's name, save size and region
strcpy(entry.name, "MYGAME");
entry.blocks = 1;
entry.region = 0x45;
// Write the updated entry to Controller Pak
write_mempak_entry_data(i, &entry, data);
// And things
free( data );
return;
}
}
}
}
}
}
Deleting is much simpler, since you only need to find the slot like in the previous section and call in this function:
// Function prototype
int delete_mempak_entry( int controller, entry_structure_t *entry );
// Sample use in the above example
delete_mempak_entry(i, &entry);
Reading/writing Controller Pak data entries
Now that we have the entry that we want, it’s time to start interacting with it.
To read you need to use this function once you have found the data save entry:
// Function prototype
int read_mempak_entry_data(int controller, entry_structure_t *entry, uint8_t *data);
// Example reading data from raw binary to a struct
// Just make sure the data fits in the right number of blocks
uint8_t* data = malloc(entry.blocks * MEMPAK_BLOCK_SIZE)
read_mempak_entry_data(i, &entry, data);
And writing works very similarly:
// Function prototype
int write_mempak_entry_data( int controller, entry_structure_t *entry, uint8_t *data );
// Example writing data from raw binary to a struct
// Just make sure the data fits in the right number of blocks
uint8_t* data = malloc(entry.blocks * MEMPAK_BLOCK_SIZE)
/* Populate the data with something */
write_mempak_entry_data(i, &entry, data);
Of course, both these functions accept a buffer of uint8_t
, but you can cast a struct to it if you want, as long as it fits within the allocated blocks.
Working with Controller Pak sectors
While the recommended way of working with Controller Pak save files is to use entries, you can also control sectors of raw memory as well. This is only recommended if you’re going to be using the entire controller pak for one save file since it is likely to overwrite other game’s save data.
A Controller Pak contains 128 sectors of memory, each being 1 block in size (256 bytes) for a total of 32kb (256 kbit).
Reading and writing to a sector
This works very similarly to working with entries except that we don’t have to do the whole “find an entry” process. All you need to do is pick a controller, pick a sector and tell it which buffer to copy data from.
// Function prototypes
int read_mempak_sector(int controller, int sector, uint8_t *sector_data);
int write_mempak_sector(int controller, int sector, uint8_t *sector_data);
// Example of filling a Controller Pak with zeroes
uint8_t data = calloc(MEMPAK_BLOCK_SIZE * 128, sizeof(uint8_t));
for (int i=0; i<128; i++) {
write_mempak_sector(0, i, data + (i*MEMPAK_BLOCK_SIZE));
}
// An example for reading a whole Controller Pak to memory
uint8_t data = malloc(MEMPAK_BLOCK_SIZE * 128 * sizeof(uint8_t));
for (int i=0; i<128; i++) {
read_mempak_sector(0, i, data + (i*MEMPAK_BLOCK_SIZE));
}