# How Lighthouse Handles and Emulates the N64 Save Format: A Deep Dive into EEPROM Emulation

> Discover how Lighthouse emulates the N64 EEPROM save format. Learn about its block-based save structure mapping and hardware-accurate read/write implementations.

- Repository: [Harbour Masters/Lighthouse](https://github.com/HarbourMasters/Lighthouse)
- Tags: deep-dive
- Published: 2026-08-04

---

**Lighthouse emulates the Nintendo 64's EEPROM save system by mapping the console's raw 8-byte block-based save structure to a host filesystem file, using `SaveData` struct definitions from [`include/save.h`](https://github.com/HarbourMasters/Lighthouse/blob/main/include/save.h) and hardware-accurate `eeprom_readBlocks`/`eeprom_writeBlocks` implementations in [`src/port/OS/libultra.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/OS/libultra.c).**

The HarbourMasters/Lighthouse project is a PC port of a classic N64 collectathon platformer. Preserving the original game's save system was essential for accuracy—so the developers built a complete EEPROM emulation layer rather than replacing it with modern alternatives. This article explains how Lighthouse handles and emulates the N64 save format, from binary layout to file I/O operations.

## The SaveData Structure: Binary Layout from include/save.h

The foundation of N64 save format emulation in Lighthouse is the `SaveData` struct defined in [`include/save.h`](https://github.com/HarbourMasters/Lighthouse/blob/main/include/save.h). This header mirrors the exact binary layout that the original game expected from N64 EEPROM hardware.

The `SaveData` structure contains:

- **Magic byte** (`0x11`) at a fixed `baseOffset` — identifies valid save data
- **Progress flags** — bitfield tracking storyline milestones
- **Collectible arrays** — separate sections for jiggies, honeycombs, Mumbo tokens, high-note scores, and time scores
- **Ability flags** — bits indicating unlocked player abilities

All fields are packed to match the original N64 memory layout. This lets the ported game code read and write save data without modification—the same pointer arithmetic and array indexing works on PC as it did on console.

## EEPROM Block I/O: libultra.c Hardware Abstraction

Lighthouse implements the N64's 8-byte block EEPROM API in [`src/port/OS/libultra.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/OS/libultra.c). These functions provide the bridge between the game's original save code and the host filesystem:

```c
s32 eeprom_readBlocks(s32 file, s32 offset, void *buffer, s32 count);
s32 eeprom_writeBlocks(s32 file, s32 offset, void *buffer, s32 count);

```

Both functions:

1. Accept a `file` argument (always `0` for the main save slot, matching single-EEPROM N64 consoles)
2. Operate on 8-byte blocks at the specified `offset`
3. Transfer `count` blocks between the host save file and the provided `buffer`

The implementation uses standard host file I/O (`fread`/`fwrite`) on a file path supplied by the launcher. The "EEPROM" is simply a regular file on disk—typically named with an `.eeprom` or `.sav` extension.

## Core Save Logic: savedata.c Load, Verify, and Create

High-level save operations live in [`src/core2/savedata.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/core2/savedata.c), which coordinates between the hardware abstraction layer and the game's state management.

### Loading Save Data

```c
void saveData_load(void *savedata_);

```

Called at `line 378` of [`savedata.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/savedata.c) during startup, this function:

1. Casts `savedata_` to `SaveData *`
2. Invokes helper functions like `__savedata_load_jiggyScore`, `__savedata_load_honeycombScore`, etc.
3. Copies each section of raw EEPROM into corresponding in-memory arrays

These helper functions establish pointer variables (`jiggy_addr`, `honeycomb_addr`, etc.) that the rest of the game uses to check and update progress.

### Verification

```c
s32 savedata_verify(size_t size, SaveData *savedata);

```

This function validates:

- Data size matches expected `SaveData` structure size
- Magic byte (`0x11`) is present at `baseOffset`

**CRC handling** deserves special mention: `savedata_update_crc` is stubbed out in the PC port. The original N64 game used CRC checks for data integrity, but Lighthouse relies on the host filesystem's built-in guarantees instead.

### Creating and Writing Saves

```c
void saveData_create(SaveData *savedata);     // Initializes struct, writes magic byte
void savedata_8033CA9C(SaveData *savedata);   // Read from EEPROM file
void savedata_8033CA2C(s32 file, SaveData *savedata);  // Write to EEPROM file

```

The naming convention `savedata_8033CA9C` and `savedata_8033CA2C` preserves original function addresses from the N64 ROM—common in decompilation projects to maintain traceability to the source binary.

## Port-Specific Integration: SaveManager and SaveConverter

Modern conveniences layer on top of the core emulation in `src/port/Save/`:

### SaveManager.cpp

Coordinates save operations with the UI, randomizer, and tracker systems. Handles:

- Save slot selection
- Auto-save triggers
- Integration with port-specific enhancements

### SaveConverter.cpp

Provides utilities for importing and exporting saves in external formats (JSON). All conversions ultimately funnel through the core `SaveData` layout—external formats are translated to and from the binary EEPROM structure, never replacing it.

## Integration with Randomizer and Tracker Systems

The N64 save format emulation enables advanced features without breaking compatibility:

- **Randomizer module** (`src/port/Rando/*`) — Uses standard [`save.h`](https://github.com/HarbourMasters/Lighthouse/blob/main/save.h) interfaces to read/write custom flags controlling shuffled collectible locations. These flags occupy unused bits in the original `SaveData` structure or extend into conventionally unused regions.

- **Live tracker overlay** ([`src/port/Enhancements/Trackers/DisplayOverlay.cpp`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/Enhancements/Trackers/DisplayOverlay.cpp)) — Reads the in-memory save arrays (populated from EEPROM via `saveData_load`) to display real-time progress without accessing the file directly.

## Practical Code Examples

### Boot Sequence Save Loading

```c
void init_save_system(void) {
    SaveData save;
    
    // Load EEPROM file into the SaveData struct
    savedata_8033CA9C(&save);          // reads from host file
    
    // Populate in-memory structures from raw data
    saveData_load(&save);
}

```

### Save on Exit

```c
void write_save_on_exit(void) {
    SaveData save;
    
    // Initialize structure with magic byte
    saveData_create(&save);
    
    // ... game updates the various save arrays ...
    
    // Write back to host EEPROM file
    savedata_8033CA2C(0, &save);
}

```

### Checking Collectible Status

```c
bool has_jiggy3(void) {
    extern u8 *jiggy_addr;   // set up by __savedata_load_jiggyScore
    
    // Array is 0-based; value 1 = obtained
    return jiggy_addr[2] != 0;
}

```

## Key Implementation Files

| File | Purpose |
|------|---------|
| [`include/save.h`](https://github.com/HarbourMasters/Lighthouse/blob/main/include/save.h) | `SaveData` struct definition — binary EEPROM layout |
| [`src/core2/savedata.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/core2/savedata.c) | Core load/save logic, verification, CRC handling |
| [`src/port/OS/libultra.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/OS/libultra.c) | `eeprom_readBlocks` / `eeprom_writeBlocks` host I/O |
| [`src/port/Save/SaveManager.cpp`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/Save/SaveManager.cpp) | High-level save coordination with UI |
| [`src/port/Save/SaveConverter.cpp`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/Save/SaveConverter.cpp) | Format conversion utilities (JSON, etc.) |

## Summary

- Lighthouse emulates the N64 save format through **block-accurate EEPROM abstraction** — the original game's 8-byte block operations work unchanged
- The `SaveData` struct in [`include/save.h`](https://github.com/HarbourMasters/Lighthouse/blob/main/include/save.h) preserves **exact binary compatibility** with the N64 original
- `eeprom_readBlocks` and `eeprom_writeBlocks` in [`src/port/OS/libultra.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/OS/libultra.c) map hardware operations to **host filesystem calls**
- [`src/core2/savedata.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/core2/savedata.c) provides **high-level save management** with verification and helper-populated in-memory arrays
- Modern features (randomizer, trackers, JSON export) layer on top without **modifying the core binary format**

## Frequently Asked Questions

### What file format does Lighthouse use for saves on PC?

Lighthouse stores saves as binary files that match the original N64 EEPROM layout exactly—typically with `.eeprom` or `.sav` extensions. While [`SaveConverter.cpp`](https://github.com/HarbourMasters/Lighthouse/blob/main/SaveConverter.cpp) can export to JSON for portability, the native format is byte-identical to what the original console produced.

### Can I use original N64 save files with Lighthouse?

Yes, provided the `SaveData` structure layout matches. The magic byte verification in `savedata_verify` will accept valid original saves. Differences in CRC handling (stubbed in Lighthouse) may require minor adjustments, but the core collectible data is directly compatible.

### Why does Lighthouse stub the CRC check?

The original N64 game used CRC checks because EEPROM hardware could corrupt data. On PC, the host filesystem provides its own integrity guarantees, making software CRC redundant. The `savedata_update_crc` function exists for compatibility but performs no operation.

### How does the randomizer store its configuration?

The randomizer uses unused bits within the standard `SaveData` structure and conventionally unused EEPROM regions. It calls the same `eeprom_readBlocks`/`eeprom_writeBlocks` functions as the base game, ensuring shuffled settings persist in the emulated N64 save format alongside original game progress.