The file format for a Hey! Pikmin saved game. Note: this comes from analyses of the .sav
files generated by Citra.
The save data is split into several blocks that start with 4-byte magic words.
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0000 |
0x0000 |
4 bytes |
Block magic word |
Always SAVE .
|
0x000C |
0x000C |
4 bytes |
Save data checksum |
Calculated using this method.
|
What "news" the player has seen. "News" are the cutscenes that take place inside the S.S. Dolphin II, where the ship explains something to Olimar.
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0010 |
0x0000 |
4 bytes |
Block magic word |
Always NEWS .
|
0x0014 |
0x0004 |
4 bytes |
Unknown |
|
0x0018 |
0x0008 |
4 bytes |
Unknown news controller 1 |
0 when the player hasn't heard the news about 1-A yet, 1 when they have.
|
0x001C |
0x000C |
4 bytes |
Unknown news controller 2 |
0 when the player hasn't heard the news about 1-A yet, 8 when they have.
|
The player's preferences in the options menu.
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0020 |
0x0000 |
4 bytes |
Block magic word |
Always OPTI .
|
0x0024 |
0x0004 |
4 bytes |
Unknown[unsure] |
|
0x0028 |
0x0008 |
1 byte |
Music volume |
0 to 3.
|
0x0029 |
0x0009 |
1 byte |
Sound effects volume |
0 to 3.
|
0x002A |
0x000A |
2 bytes |
Unknown[unsure] |
|
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x002C |
0x0000 |
4 bytes |
Block magic word |
Always PBUF .
|
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0080 |
0x0000 |
4 bytes |
Block magic word |
Always PARK .
|
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x04A4 |
0x0000 |
4 bytes |
Block magic word |
Always MINI .
|
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0588 |
0x0000 |
4 bytes |
Block magic word |
Always CRNT .
|
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x09A8 |
0x0000 |
4 bytes |
Block magic word |
Always GAME .
|
0x09AC |
0x0004 |
4 bytes |
Unknown[unsure] |
|
0x09B0 |
0x0008 |
4 bytes |
Sparklium total |
Values over 99999 will make the game assume it's just 99999
|
0x09B4 |
0x000C |
44 bytes |
Unknown[unsure] |
|
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x09E0 |
0x0000 |
4 bytes |
Block magic word |
Always EVNT .
|
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0AE8 |
0x0000 |
4 bytes |
Block magic word |
Always HELP .
|
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0BF0 |
0x0000 |
4 bytes |
Block magic word |
Always 0xRINF .
|
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0C2C |
0x0000 |
4 bytes |
Block magic word |
Always RSLT .
|
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0C38 |
0x0000 |
4 bytes |
Block magic word |
Always SPER .
|
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0C48 |
0x0000 |
4 bytes |
Block magic word |
Always STRE .
|
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0C90 |
0x0000 |
4 bytes |
Block magic word |
Always PLRP .
|
Time at which the save was made.
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0CE0 |
0x0000 |
4 bytes |
Block magic word |
Always DATE .
|
0x0CE4 |
0x0004 |
8 bytes |
Unknown[unsure] |
|
0x0CEC |
0x000C |
1 byte |
Month |
1 to 12.
|
0x0CED |
0x000D |
1 byte |
Day |
1 to 31.
|
0x0CEE |
0x000E |
1 byte |
Hours |
0 to 23.
|
0x0CEF |
0x000F |
1 byte |
Minutes |
0 to 59.
|
0x0CF0 |
0x0010 |
1 byte |
Seconds |
0 to 59.
|
0x0CF1 |
0x0011 |
11 bytes |
Unknown[unsure] |
|
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0CFC |
0x0000 |
4 bytes |
Block magic word |
Always TTDS .
|
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0D08 |
0x0000 |
4 bytes |
Block magic word |
Always WMAP .
|
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0D80 |
0x0000 |
4 bytes |
Block magic word |
Always SAMI .
|
The checksum seems to be a CRC32 of the 0xd87 bytes that follow after the checksum (i.e. just one byte short of the entire remaining data), with crc32 as implemented by e.g. zlib.
The following python code can recalculate the checksum if the data after the checksum is modified:
import struct
from zlib import crc32
filepath = "radish0.sav" # put the script in the same folder as the save file
with open(filepath, "rb") as f:
header = f.read(0xC)
chksum = struct.unpack("I", f.read(4))[0]
data = f.read()
calculated_chksum = crc32(data[0:0xd87])
with open(filepath, "wb") as f:
f.write(header)
f.write(struct.pack("I", calculated_chksum))
f.write(data)