Chapter 6
The save
Everything the game knows about your relationship fits in 256 KB of battery-backed memory on the cartridge. The save format is careful: every block is stored twice, checksummed and compressed, so a save interrupted halfway never loses the one before it.
Six kinds of block
The save is made of blocks, each tagged with four letters:
| block | tag | holds | size in memory |
|---|---|---|---|
| A | LDSD | settings: music, sound and voice volume, text speed | 504 B |
| B | LDGD | the game itself: names, the calendar, every flag, the three heroines' records | 15,448 B |
| C | LDOD | a small header block | 244 B |
| D | LDPD | event state, written from the save menu | 4,098 B |
| E | LDFD | 100 small records written only by the wireless screens | 196 B each |
| F | LDTG | one large record per heroine, most likely the trips | 5,320 B each |
Block B is the one that matters. The game keeps it in memory as one structure, the GameState, and saves it whole.
Two copies, always
Every block has two places on the cartridge, used in turn. Each copy starts with a 32-byte header: a magic number, the stored size, a generation counter, a version, and a SHA-1 hash of the data. When the game saves, it writes to the copy it didn't load from, with the generation one higher. When it loads, it takes the newer copy whose magic, size and hash all check out. If the power fails mid-write, the half-written copy fails its hash and the game falls back to the other one. Save_WriteBlock does the writing (it was labelled as a sprite function until we proved what it does).
Packed, then compressed
Before a block is written, overlay 0 packs it (Ov00Init_PackSaveB and its siblings). Packing splits the structure into sections and rearranges its fields down to the bit: narrowing numbers, folding flags together, splitting records. Block B goes into 73 sections. Then the packed block is compressed with Nintendo's LZ compression (MI_CompressLZImpl), or stored as it is when compression wouldn't make it smaller.
About 200 small routines do the packing. Rather than read them all, we let the game describe itself: the game's own packer and unpacker were run on test patterns, where pattern k sets every input bit whose number has bit k set, each paired with its opposite. Every output bit then gives away which input bit it came from. The result: each kind of block is a pure rearrangement of bits, with one exception, a run of 167 bytes that are saved as true or false.
What it remembers
A few things in the GameState:
- Your names. Two name fields, filled from the profile you write by hand.
- Where the story stands. A field records whether you're in the main story or have become her boyfriend.
- When you last saved. Year, month, day, weekday and time, which the game compares with the clock (chapter 4).
- A block per heroine. Each of the three gets 1,672 bytes of her own: thousands of flags (bits), counters (bytes), larger values (words), and her record.
Flags work in a slightly surprising way. A flag that belongs to no one in particular is written into all three heroines' blocks, and read back from Manaka's (GameFlags_SetScopedBit). There's no separate global block; there are three copies.
Her record
Each heroine's record (GameState_GetSlotPtr) is where the relationship lives:
| field | what it is |
|---|---|
| flags | whether you're together; whether it ended |
| phase | the relationship stage, 0 to 2: the column every scene table in chapter 3 is indexed by |
| affection | a value from 0 to 255, shown as hearts. SaveSlot_GetLevelFromPoints turns it into 0 to 16 hearts, with a threshold table per heroine |
| love points | once you're together, a second, larger counter (SaveSlot_AddPointsClamped) with 20 levels, from 100 points up to 35,500 |
| more | a mood byte, counters, timers and small tables still being read |
In lp-engine
The save is a plain 256 KB file, saves/loveplus.sav, byte for byte what a cartridge would hold. That
makes saves portable: Game > Import Save takes one dumped from a real cartridge or from DeSmuME, and
Export Save gives it back. The game writes the save itself, exactly as on the DS; the engine stores the result.
lp-engine also has its own reader and writer for the format, used by its tools and tests.
How we know
- Our compressor produces the game's exact bytes on every recorded block. Matching it took the game's own search order: farthest match first, never closer than two bytes, at most 18 bytes long.
- For every kind of block, across several slots and every copy and generation state, our card writer and the
game's
Save_WriteBlockproduced identical bytes: 30 writes out of 30. - Saves edited by our writer boot in the game, which then holds exactly the GameState we wrote: 15,448 bytes out of 15,448.
Still open. Blocks D and F are proven against the game's code, but no real save we have contains them yet. They belong to the later part of the game, after you start going out. Block E is written only by the wireless features. Many single bytes in the heroine record are typed but not yet named. The details are in docs/SAVE_FORMAT.md and docs/SAVE_RECORD.md.