Difference between revisions of "Hey! Pikmin save file"
(Better offsets.) |
m |
||
(7 intermediate revisions by 3 users not shown) | |||
Line 3: | Line 3: | ||
== Format == | == Format == | ||
− | The save data is split into several blocks that start with 4-byte magic words. | + | 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 === | === SAVE === | ||
Line 11: | Line 11: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x0000</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>SAVE</code>. |
|- | |- | ||
− | | <code> | + | | <code>0x000C</code> || <code>0x000C</code> || 4 bytes || Save data checksum || Calculated using [[#Checksum|this method]]. |
|} | |} | ||
=== NEWS === | === 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. | ||
{| class="wikitable sortable" | {| class="wikitable sortable" | ||
! colspan="2" | Offset || rowspan="2" | Length || rowspan="2" | Field || rowspan="2" | Notes | ! colspan="2" | Offset || rowspan="2" | Length || rowspan="2" | Field || rowspan="2" | Notes | ||
Line 22: | Line 23: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x0010</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>NEWS</code>. |
|- | |- | ||
+ | | <code>0x0014</code> || <code>0x0004</code> || 4 bytes || Unknown || | ||
+ | |- | ||
+ | | <code>0x0018</code> || <code>0x0008</code> || 4 bytes || Unknown news controller 1 || 0 when the player hasn't heard the news about 1-A yet, 1 when they have. | ||
+ | |- | ||
+ | | <code>0x001C</code> || <code>0x000C</code> || 4 bytes || Unknown news controller 2 || 0 when the player hasn't heard the news about 1-A yet, 8 when they have. | ||
|} | |} | ||
=== OPTI === | === OPTI === | ||
+ | The player's preferences in the options menu. | ||
{| class="wikitable sortable" | {| class="wikitable sortable" | ||
! colspan="2" | Offset || rowspan="2" | Length || rowspan="2" | Field || rowspan="2" | Notes | ! colspan="2" | Offset || rowspan="2" | Length || rowspan="2" | Field || rowspan="2" | Notes | ||
Line 32: | Line 39: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x0020</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>OPTI</code>. |
+ | |- | ||
+ | | <code>0x0024</code> || <code>0x0004</code> || 4 bytes || {{unknown|Unknown}} || | ||
+ | |- | ||
+ | | <code>0x0028</code> || <code>0x0008</code> || 1 byte || Music volume || 0 to 3. | ||
|- | |- | ||
− | | <code> | + | | <code>0x0029</code> || <code>0x0009</code> || 1 byte || Sound effects volume || 0 to 3. |
|- | |- | ||
+ | | <code>0x002A</code> || <code>0x000A</code> || 2 bytes || {{unknown|Unknown}} || | ||
|} | |} | ||
=== PBUF === | === PBUF === | ||
+ | Which log entries the player has unlocked, and which they have read. | ||
{| class="wikitable sortable" | {| class="wikitable sortable" | ||
! colspan="2" | Offset || rowspan="2" | Length || rowspan="2" | Field || rowspan="2" | Notes | ! colspan="2" | Offset || rowspan="2" | Length || rowspan="2" | Field || rowspan="2" | Notes | ||
Line 44: | Line 57: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x002C</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>PBUF</code>. |
|- | |- | ||
+ | | <code>0x0030</code> || <code>0x0004</code> || 4 bytes || Unknown || Messing with it can make the save file be recognized as empty in the title screen. | ||
+ | |- | ||
+ | | <code>0x0034</code> || <code>0x0008</code> || 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. | ||
+ | |- | ||
+ | | <code>0x0058</code> || <code>0x002C</code> || 4 bytes || Read "Pikmin Logs" entries bitfield || | ||
+ | |- | ||
+ | | <code>0x005C</code> || <code>0x0030</code> || 36 bytes || Read entries from the other categories bitfield || | ||
|} | |} | ||
Line 54: | Line 74: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x0080</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>PARK</code>. |
|- | |- | ||
|} | |} | ||
Line 64: | Line 84: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x04A4</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>MINI</code>. |
|- | |- | ||
|} | |} | ||
Line 74: | Line 94: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x0588</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>CRNT</code>. |
|- | |- | ||
|} | |} | ||
Line 84: | Line 104: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x09A8</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>GAME</code>. |
+ | |- | ||
+ | | <code>0x09AC</code> || <code>0x0004</code> || 4 bytes || {{unknown|Unknown}} || | ||
+ | |- | ||
+ | | <code>0x09B0</code> || <code>0x0008</code> || 4 bytes || Sparklium total || Values over 99999 will make the game assume it's just 99999 | ||
+ | |- | ||
+ | | <code>0x09B4</code> || <code>0x000C</code> || 44 bytes || {{unknown|Unknown}} || | ||
|- | |- | ||
|} | |} | ||
Line 94: | Line 120: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x09E0</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>EVNT</code>. |
|- | |- | ||
|} | |} | ||
Line 104: | Line 130: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x0AE8</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>HELP</code>. |
|- | |- | ||
|} | |} | ||
Line 114: | Line 140: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x0BF0</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>RINF</code>. |
|- | |- | ||
|} | |} | ||
Line 124: | Line 150: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x0C2C</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>RSLT</code>. |
|- | |- | ||
|} | |} | ||
Line 134: | Line 160: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x0C38</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>SPER</code>. |
|- | |- | ||
|} | |} | ||
Line 144: | Line 170: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x0C48</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>STRE</code>. |
|- | |- | ||
|} | |} | ||
Line 154: | Line 180: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x0C90</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>PLRP</code>. |
|- | |- | ||
|} | |} | ||
=== DATE === | === DATE === | ||
+ | Time at which the save was made. | ||
+ | |||
{| class="wikitable sortable" | {| class="wikitable sortable" | ||
! colspan="2" | Offset || rowspan="2" | Length || rowspan="2" | Field || rowspan="2" | Notes | ! colspan="2" | Offset || rowspan="2" | Length || rowspan="2" | Field || rowspan="2" | Notes | ||
Line 164: | Line 192: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x0CE0</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>DATE</code>. |
+ | |- | ||
+ | | <code>0x0CE4</code> || <code>0x0004</code> || 4 bytes || {{unknown|Unknown}} || | ||
+ | |- | ||
+ | | <code>0x0CE8</code> || <code>0x0008</code> || 4 bytes || Year || Human format. For instance, 2019 has the actual value "2019". | ||
+ | |- | ||
+ | | <code>0x0CEC</code> || <code>0x000C</code> || 1 byte || Month || 1 to 12. | ||
+ | |- | ||
+ | | <code>0x0CED</code> || <code>0x000D</code> || 1 byte || Day || 1 to 31. | ||
|- | |- | ||
+ | | <code>0x0CEE</code> || <code>0x000E</code> || 1 byte || Hours || 0 to 23. | ||
+ | |- | ||
+ | | <code>0x0CEF</code> || <code>0x000F</code> || 1 byte || Minutes || 0 to 59. | ||
+ | |- | ||
+ | | <code>0x0CF0</code> || <code>0x0010</code> || 1 byte || Seconds || 0 to 59. | ||
+ | |- | ||
+ | | <code>0x0CF1</code> || <code>0x0011</code> || 11 bytes || {{unknown|Unknown}} || | ||
|} | |} | ||
Line 174: | Line 217: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x0CFC</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>TTDS</code>. |
|- | |- | ||
|} | |} | ||
Line 184: | Line 227: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x0D08</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>WMAP</code>. |
|- | |- | ||
|} | |} | ||
Line 194: | Line 237: | ||
! File || Block | ! File || Block | ||
|- | |- | ||
− | | <code> | + | | <code>0x0D80</code> || <code>0x0000</code> || 4 bytes || Block magic word || Always <code>SAMI</code>. |
|- | |- | ||
|} | |} | ||
== Checksum == | == 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: | ||
+ | |||
+ | <pre> | ||
+ | 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) | ||
+ | </pre> | ||
− | * | + | == Tools == |
− | + | * [https://github.com/Espyo/Misc_Pikmin_tools/tree/master/hp_save_editor Hey! Pikmin save editor] | |
− | |||
− | |||
− | |||
− | |||
− | |||
[[Category:File formats]] | [[Category:File formats]] | ||
[[Category:Hey! Pikmin]] | [[Category:Hey! Pikmin]] |
Latest revision as of 14:27, 11 September 2022
This article is a stub.
The file format for a Hey! Pikmin saved game. Note: this comes from analyses of the .sav
files generated by Citra.
Contents
Format[edit]
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[edit]
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[edit]
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[edit]
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[edit]
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[edit]
Offset | Length | Field | Notes | |
---|---|---|---|---|
File | Block | |||
0x0080 |
0x0000 |
4 bytes | Block magic word | Always PARK .
|
MINI[edit]
Offset | Length | Field | Notes | |
---|---|---|---|---|
File | Block | |||
0x04A4 |
0x0000 |
4 bytes | Block magic word | Always MINI .
|
CRNT[edit]
Offset | Length | Field | Notes | |
---|---|---|---|---|
File | Block | |||
0x0588 |
0x0000 |
4 bytes | Block magic word | Always CRNT .
|
GAME[edit]
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[edit]
Offset | Length | Field | Notes | |
---|---|---|---|---|
File | Block | |||
0x09E0 |
0x0000 |
4 bytes | Block magic word | Always EVNT .
|
HELP[edit]
Offset | Length | Field | Notes | |
---|---|---|---|---|
File | Block | |||
0x0AE8 |
0x0000 |
4 bytes | Block magic word | Always HELP .
|
RINF[edit]
Offset | Length | Field | Notes | |
---|---|---|---|---|
File | Block | |||
0x0BF0 |
0x0000 |
4 bytes | Block magic word | Always RINF .
|
RSLT[edit]
Offset | Length | Field | Notes | |
---|---|---|---|---|
File | Block | |||
0x0C2C |
0x0000 |
4 bytes | Block magic word | Always RSLT .
|
SPER[edit]
Offset | Length | Field | Notes | |
---|---|---|---|---|
File | Block | |||
0x0C38 |
0x0000 |
4 bytes | Block magic word | Always SPER .
|
STRE[edit]
Offset | Length | Field | Notes | |
---|---|---|---|---|
File | Block | |||
0x0C48 |
0x0000 |
4 bytes | Block magic word | Always STRE .
|
PLRP[edit]
Offset | Length | Field | Notes | |
---|---|---|---|---|
File | Block | |||
0x0C90 |
0x0000 |
4 bytes | Block magic word | Always PLRP .
|
DATE[edit]
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[edit]
Offset | Length | Field | Notes | |
---|---|---|---|---|
File | Block | |||
0x0CFC |
0x0000 |
4 bytes | Block magic word | Always TTDS .
|
WMAP[edit]
Offset | Length | Field | Notes | |
---|---|---|---|---|
File | Block | |||
0x0D08 |
0x0000 |
4 bytes | Block magic word | Always WMAP .
|
SAMI[edit]
Offset | Length | Field | Notes | |
---|---|---|---|---|
File | Block | |||
0x0D80 |
0x0000 |
4 bytes | Block magic word | Always SAMI .
|
Checksum[edit]
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)