♥ lp-engine

Chapter 1

The machine

LovePlus+ was written for a computer with two processors, 4 MB of memory and a cartridge it reads as it goes. Most of what's odd about the game's code makes sense once you know the box it was squeezed into.

Two processors

The Nintendo DS has two ARM processors that work side by side:

processorruns
ARM9, 67 MHzThe game: its logic, scripts, both screens' graphics, the 3D. Everything in the function map runs here.
ARM7, 33 MHzNintendo's own program for the hardware the game doesn't touch directly: sound mixing, the touch screen, the microphone, the clock chip, power and wireless.

The two talk through a small hardware queue, the PXI FIFO. Each message carries a tag naming the service it's for (7 is sound, 9 the microphone, 10 wireless), and each side registers a handler per tag. When the game wants a sound played, it builds a list of commands in shared memory and sends its address over tag 7. When it wants a microphone sample, it sends a short command over tag 9 and waits for the answer. PXI_SendWordByFifo is the door every one of those requests goes through.

Memory

The ARM9 has 4 MB of main memory, plus two small fast memories built into the processor: 32 KB for code and 16 KB for data. LovePlus+ uses all of them deliberately:

Overlays

1.2 MB of always-loaded code leaves too little room for everything else, so most of the game lives in overlays: blocks of code loaded from the cartridge when they're needed and dropped when they aren't. LovePlus+ has 44 of them, in two regions of memory:

regionoverlayslargestwhat lives there
0x02158280669 KBstart-up, the scenes (dates and skinship), schedule input, maps, speech recognition
0x0216A92038157 KBone screen at a time: the title menu, file select, profile, calendar, gallery, the room, overnight, wireless…

Overlays in the same region share addresses, so only one per region can be in memory at once. That's why the function map always shows a module next to an address: 0x0216C800 is a file-delete function in overlay 28 and an empty stub in the room's overlay 35. The game loads and drops them with Overlay_Load and Overlay_Unload; the screen manager from chapter 2 does it for every screen.

Overlay 1 is different. It's the game's copy protection, and its code decrypts itself when it runs. lp-engine leaves it alone: the game decrypts it at run time, as it always did, and our tools never do.

Three layers of code

The ARM9 program is built from three layers, and the function map labels each one:

layerfunctionswhat it is
Nintendo SDK962The operating system: threads, interrupts, the file system, the cartridge, PXI, timers, the 2D and 3D hardware. Names like OS_IrqHandler.
NitroSystem292Nintendo's middleware: the 3D model and animation runtime, 2D cells and animations, the sound sequencer. Names like NNSi_G3dFuncSbc_NODEDESC.
The game13,897Konami's own code: everything from the state machines to the voice decoder.

The library functions were identified by matching them against the libraries themselves, so their names are exact. That matters beyond the libraries: every game function that calls PM_GoSleepMode or MIC_StartAutoSamplingAsync has just told you a lot about itself.

Time

The game measures time in ticks of the SDK's clock, 64 cycles of the DS's 33.5 MHz bus, or about 524,000 a second. The screens refresh every 8,753 ticks, a little under 60 times a second. Ticks drive the game's alarms and waits; the screen refresh drives its frames. The calendar is a third clock, kept by the ARM7's real-time clock chip, and it gets a chapter of its own.

The cartridge

The cartridge is 512 MB, though the game's file table lists only 301 files. Most of them are archives in a container of the game's own, which hold about 19,000 files between them: Nintendo's standard 2D and 3D formats, the sound archive, and formats of Konami's own:

The scripts and text are lightly scrambled, with the key stored in each file's own header. Reading them is ordinary format parsing.

How lp-engine runs it

lp-engine isn't an emulator, but it isn't a rewrite of the game either. It sits in between:

Ported functions are checked against the game's own: the same inputs must produce the same memory and the same return value. That's how the game can be moved over to native code a function at a time, without ever breaking.