The file format for a Hey! Pikmin saved game. Note: this comes from analyses of the .sav
files generated by Citra.
Format
The save data is split into several blocks that start with 4-byte magic words. The blocks are ordered in the same order they are presented in this article.
SAVE
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.
|
NEWS
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.
|
OPTI
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] |
|
PBUF
Which log entries the player has unlocked, and which they have read.
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x002C |
0x0000 |
4 bytes |
Block magic word |
Always PBUF .
|
0x0030 |
0x0004 |
4 bytes |
Unknown |
Messing with it can make the save file be recognized as empty in the title screen.
|
0x0034 |
0x0008 |
36 bytes |
Unlocked entries bitfield |
The first 11 bits correspond to the Pikmin Logs category (blue pellet, red pellet, rock pellet, winged pellet, yellow pellet, Onion, Red Pikmin, Blue Pikmin, Yellow Pikmin, Rock Pikmin, Winged Pikmin). After that come the enemies, normal treasures, and amiibo treasures, but their order is all over the place.
|
0x0058 |
0x002C |
4 bytes |
Read "Pikmin Logs" entries bitfield |
|
0x005C |
0x0030 |
36 bytes |
Read entries from the other categories bitfield |
|
PARK
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0080 |
0x0000 |
4 bytes |
Block magic word |
Always PARK .
|
MINI
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x04A4 |
0x0000 |
4 bytes |
Block magic word |
Always MINI .
|
CRNT
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0588 |
0x0000 |
4 bytes |
Block magic word |
Always CRNT .
|
GAME
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] |
|
EVNT
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x09E0 |
0x0000 |
4 bytes |
Block magic word |
Always EVNT .
|
HELP
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0AE8 |
0x0000 |
4 bytes |
Block magic word |
Always HELP .
|
RINF
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0BF0 |
0x0000 |
4 bytes |
Block magic word |
Always RINF .
|
RSLT
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0C2C |
0x0000 |
4 bytes |
Block magic word |
Always RSLT .
|
SPER
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0C38 |
0x0000 |
4 bytes |
Block magic word |
Always SPER .
|
STRE
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0C48 |
0x0000 |
4 bytes |
Block magic word |
Always STRE .
|
PLRP
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0C90 |
0x0000 |
4 bytes |
Block magic word |
Always PLRP .
|
DATE
Time at which the save was made.
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0CE0 |
0x0000 |
4 bytes |
Block magic word |
Always DATE .
|
0x0CE4 |
0x0004 |
4 bytes |
Unknown[unsure] |
|
0x0CE8 |
0x0008 |
4 bytes |
Year |
Human format. For instance, 2019 has the actual value "2019".
|
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] |
|
TTDS
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0CFC |
0x0000 |
4 bytes |
Block magic word |
Always TTDS .
|
WMAP
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0D08 |
0x0000 |
4 bytes |
Block magic word |
Always WMAP .
|
SAMI
Offset |
Length |
Field |
Notes
|
File |
Block
|
0x0D80 |
0x0000 |
4 bytes |
Block magic word |
Always SAMI .
|
Checksum
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)
Tools