♥ lp-engine

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:

blocktagholdssize in memory
ALDSDsettings: music, sound and voice volume, text speed504 B
BLDGDthe game itself: names, the calendar, every flag, the three heroines' records15,448 B
CLDODa small header block244 B
DLDPDevent state, written from the save menu4,098 B
ELDFD100 small records written only by the wireless screens196 B each
FLDTGone large record per heroine, most likely the trips5,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:

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:

fieldwhat it is
flagswhether you're together; whether it ended
phasethe relationship stage, 0 to 2: the column every scene table in chapter 3 is indexed by
affectiona value from 0 to 255, shown as hearts. SaveSlot_GetLevelFromPoints turns it into 0 to 16 hearts, with a threshold table per heroine
love pointsonce you're together, a second, larger counter (SaveSlot_AddPointsClamped) with 20 levels, from 100 points up to 35,500
morea 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

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.