This page outlines the basic structure of decrypted save files.
There are two file types, both in little-endian format for all datapoints unless otherwise specified:
-
game*.ywfiles. These hold the main save data - where the*corresponds to the save slot (i.e.,game1.ywis the first slot,game2.ywis the second, etc.). These store nearly everything and use aSectionIDformat. -
head.ywfiles, these contain the player’s name, encryption keys for V2 saves, preview data (all the data seen before you click on a save file) etc. These are encrypted identically toYW1save files - see the decrypt page for more. Despite these also using aSectionIDformat - it only has one top-levelSectionleading direct offsets to still work.
⚠️ Modifying the preview data is mostly pointless, as the game will overwrite most of it when saving - certain exceptions do exist such as the player’s name however, which can be edited persistently (although this actually changes the player’s name) - See the Header Files page for more info.
SectionID Format
Save data in game*.yw files is organized into nested sections, each defined by a pair of headers:
- Header 1 (
h1):
A 2-byteint16enum to mark section boundaries:0xFFFE= Section Start0xFEFF= Section End
- Header 2 (
h2):
A 4-byte value with the structure below:- Bits 0–7: Section ID (
uint8) - Bits 8–31: Section size in bytes (excluding the 8 bytes used by
h1andh2)
- Bits 0–7: Section ID (
Section ID (ID)
This is a Uint8 (0-255) used to uniquely identify the section, kind of like a UUID. For instance, 0x06 is the SectionID for Key Items, which is constant, and NEVER varies between save files, despite the exact offset being inconsistent, and therefore cannot directly be used. Each section however always contains the same type of data at the same offset relative to the start of the section (FE). The general structure would look something like this:
Root (ID: 1, size: 64)
├── Child (ID: 5, size: 32)
│ └── Grandchild (ID: 9, size: 8)
└── Child (ID: 6, size: 12)
Where each Section would be formatted like this:
| Offset | Bytes | Meaning |
|---|---|---|
| 0x00 | FE FF 00 00 | h1 = Section Start Marker (0xFFFE) |
| 0x04 | F1 34 15 01 | h2 = Section Header Data |
→ Reversed: 01 15 34 F1 (little-endian to big-endian) | ||
→ Full value: 0x011534F1 | ||
→ ID = 0xF1 (last byte) | ||
→ Size = 0x011534 (upper 3 bytes = 70964 bytes) | ||
| 0x08 | (data) | Payload of section ID 241 |
| … | … | Data. Can be nested. |
| 0x1153C | FF FE | End Marker (0xFEFF) |
Misc Notes
- A decrypted
game*.ywdwill have data before/after the first top-level section for re-encryption.
Common Data Types
- Integers: Typically stored as 32-bit signed values - although obviously depends on the datapoint itself.
- IDs: these are stored as a standard CRC-32 Checksum. An example of this is Location; which is stored as the CRC-32 of it’s file name. A list of all the file names can be found here. Other examples include Items and Yo-kai.
- Text: Encoding depends on region (see below).
- Bitmasks: Compact representation of boolean states (example below):
00000000000000000000 → No trophies
11111111111111111111 → All trophies
10000000000000000001 → First and last trophy only
Used for data like:
- Trophies
- Opened treasure chests
- Unlocked win poses
etc
Reigonal Differences:
- JP copies use cp932 (known as Code Page 932 or Windows 31-J) for text - which is an extension of SHIFT_JIS, whereas international save files use UTF-8. Since they both have ASCII compatibility (not extended ASCII) - the problem is usually more evident using the international system for JP saves than the other way around.
Code Examples
- SectionID parsing code (taken from an old version of my save editor):
(function (global) {
class Section {
constructor(id, size, pos) {
this.id = id;
this.size = size;
this.pos = pos;
this.children = [];
}
addChild(child) {
this.children.push(child);
}
}
function parseSavedata(fullBytes) {
if (!(fullBytes instanceof Uint8Array)) {
throw new Error("Input must be a Uint8Array.");
}
if (fullBytes.length <= 0x20) {
console.error("Input too short to skip 0x20 offset.");
return null;
}
const bytes = fullBytes.slice(0x20); // skip the part outside of the first/main SectionID (0x01)
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const stack = [];
let root = null;
let pos = 0;
function atEnd() { // self-explanatory
return pos >= bytes.length;
}
if (pos + 4 > bytes.length) return null;
let h1 = view.getUint32(pos, true);
pos += 4;
if ((h1 & 0xFFFF) !== 0xFFFE) return null; // if the low 16-bits == 0xFFFE, then continue, also gotta love magic numbers
if (pos + 4 > bytes.length) return null; // no data
let h2 = view.getUint32(pos, true);
pos += 4;
let id = h2 & 0xFF; // get the low 8-bits of h2 as id
let size = h2 >>> 8; // shift right by 8 bits — you get the remaining 24 bits as size
root = new Section(id, size, pos); // init the new sectionID
stack.push(root);
while (!atEnd()) {
if (pos + 4 > bytes.length) break; // too short/no data left
h1 = view.getUint32(pos, true);
pos += 4;
while ((h1 & 0xFFFF) === 0xFFFE) { // same check as earlier
if (pos + 4 > bytes.length) return null;
h2 = view.getUint32(pos, true);
pos += 4;
id = h2 & 0xFF; // again grab the ID
size = h2 >>> 8; // and the size
if (stack.length === 0 || pos + size > bytes.length) return null; // very sad oh noes
const sec = new Section(id, size, pos); // init a section
stack[stack.length - 1].addChild(sec); // now we
stack.push(sec); // add it to stack
if (pos + 4 > bytes.length) break; // too short/no data left
h1 = view.getUint32(pos, true);
pos += 4;
}
if ((h1 & 0xFFFF) === 0xFEFF) {
if (stack.length === 0) return null;
stack.pop();
} else {
if (stack.length === 0) {
console.warn("Data outside sections, stopping parse but exporting tree anyway.");
break; // Don't return null — just stop parsing.
}
const current = stack[stack.length - 1];
const dataToSkip = current.size - 4;
if (pos + dataToSkip > bytes.length) return null;
pos += dataToSkip;
}
}
if (!root) return null;
// Final check: if the stack is unbalanced, log a warning, but still return the result
if (stack.length !== 0) {
console.warn("Unbalanced sections: some sections were not closed properly.");
}
return sectionToObject(root);
}
function sectionToObject(section) {
return {
id: section.id,
size: section.size,
pos: section.pos,
children: section.children.map(sectionToObject),
};
}
function grabOffset(node, targetId) {
console.log(`Checking node id: ${node.id}`);
if (node.id === targetId) {
console.log(`Found id ${targetId}, pos: ${node.pos}`);
return node.pos;
}
for (const child of node.children) {
const pos = grabOffset(child, targetId);
if (pos !== null) {
return pos;
}
}
return null;
}
global.SaveDataParser = {
parseSavedata,
};
global.grabOffset = grabOffset;
global.IDObj = { /* The IDs for different data sections note that these sections dont just hold one piece of data i.e. 0x07 isnt JUST yokai - it's just the first use found for that section*/
"Yokai": "0x07",
"Item": "0x04",
"Equipment": "0x05",
"Important": "0x06",
"misc1": "0x01",
"misc9": "0x09",
"Soul": "0x13",
"Medallium": "0x01",
"misc8": "0x14"
}
global.genOffset = {
"Yokai": 0x20,
"Important": 0x20
} /* The general offset constants i.e. for Yokai it starts at the ID 0x07 but the first 0x20 bytes aren't for any Yo-kai in particular, and I havent figured out what they do, as of me writing this (I probably have, and just haven't updated this */
})(window);
Unconfirmed
- After a h2 there is always 32 bytes (
0x20) to skip past. This is correct for Key Items and Yo-kai.