A C (C17) library to read and write Nintendo 64 Controller Pak (Memory Pak) saves, with no external dependencies. It fully implements the pak file system (PFS): ID area, index tables, note table, block chaining, checksums, validation and repair.
Ships with a CLI (ctrpak) and usage examples — including a real N64 ROM via
libdragon.
$ ctrpak list mario.mpk
pak 0: 2 note(s), 117/123 blocks free
# name blocks bytes code company region start
0 MARIO KART 64 4 1024 NKTE 01 E 5
1 ZELDA.A 2 512 NZLE 01 E 9
- List, read, create, update, rename, copy and delete notes
- Format a fresh pak (valid ID block + index tables)
- Full validation (
ctrpak_pak_validate()) and repair (ctrpak_pak_repair()), in the spirit of Controller-Pak-Manager's "Press Z to repair the pak" - Defragmentation (
ctrpak_pak_defragment()) - Conversion between containers: raw 32 KiB, multi-pak (mupen64plus,
4 controllers in one file) and DexDrive
.n64(with its 16 comments) - Import/export of individual notes (
.note= 32 byte entry + data) - Conversion of the N64's own charset used in note names
- Formatted image byte for byte identical to mupen64plus'
format_mempak()(verified by test) - Error-code based API (
ctrpak_error_t), no exceptions, no C++: every result comes back through an out-parameter ctrpak_pak_t(the core: reading/creating/deleting/validating/repairing notes) never allocates memory — a fixed 32 KiB struct, nomalloc. Onlyctrpak_pak_defragment()and the container module (ctrpak_pakfile_*,ctrpak_notefile_*) allocate, and only because they handle a variable number of paks/bytes on disk- The core runs both on the desktop and embedded in an N64 ROM via libdragon (see libdragon integration)
The library is standalone (hosted C17, <stdio.h>/malloc), but the core
(ctrpak_pak_t, notes, charset, validation/repair) depends on nothing beyond
the standard library, so it also runs inside a homebrew ROM built with
libdragon (mips64-elf-gcc toolchain, n64.mk) - which is exactly how it was
verified: actually compiled with that toolchain, not just type-checked.
libdragon already handles the joybus protocol (retries, per-block CRC)
through read_mempak_sector()/write_mempak_sector() (<mempak.h>), but
only offers a pass/fail check and simple note operations. libctrpak layers
on top: platform/libdragon/ctrpak_libdragon.h
bridges the 128 joybus sectors and a ctrpak_pak_t, so you get the full API
(ctrpak_pak_validate(), ctrpak_pak_repair(), ctrpak_pak_defragment(),
rename with duplicate detection, etc.) against the physical pak:
#include "ctrpak/ctrpak.h"
#include "ctrpak_libdragon.h"
ctrpak_pak_t before, pak;
if (ctrpak_libdragon_read_pak(controller, &before) != CTRPAK_OK) {
/* accessory missing, or the pak has a physical fault */
}
pak = before; /* 32 KiB struct by value - a plain copy */
if (!ctrpak_pak_validate(&pak).ok) ctrpak_pak_repair(&pak);
ctrpak_pak_delete_note(&pak, 3, false);
ctrpak_libdragon_write_changes(controller, &before, &pak, NULL); /* only the sectors that changed */ctrpak_libdragon_write_changes() writes only the sectors that differ from
what is already on the cart - a physical write to the pak is slow, and most
edits (deleting, renaming, creating a small note) only touch the note table
and a handful of data pages.
This file is not compiled by the project's Makefile/CMakeLists (<mempak.h>
only exists inside the libdragon toolchain) - copy or add
platform/libdragon/ctrpak_libdragon.c as a source in your N64 project
alongside src/*.c. A complete ROM example (browses the notes of up to 4
controllers, deletes with A) lives in
examples/n64_libdragon/ with its own n64.mk-style
Makefile - tested by actually building browser.z64 with the real
toolchain (mips64-elf-gcc 14.4, libdragon) via
retro n64 make -C examples/n64_libdragon.
Being plain C, it depends on no exceptions, no RTTI and no libstdc++ - the
example ROM links only against libdragon.a/libdragonsys.a and comes out
noticeably smaller than the same logic in C++ would.
Without cmake:
make # library, CLI, tests and example in build/
make test # runs the 153 checks
sudo make install PREFIX=/usr/localWith cmake:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir buildIn another cmake project:
add_subdirectory(libctrpak)
target_link_libraries(my_app PRIVATE ctrpak::ctrpak)#include <ctrpak/ctrpak.h>
/* open (auto-detects raw / multi-pak / DexDrive) */
ctrpak_pakfile_t file;
ctrpak_error_t err = ctrpak_pakfile_load(&file, "mario.mpk");
if (err != CTRPAK_OK) { fprintf(stderr, "%s\n", ctrpak_error_string(err)); return 1; }
ctrpak_pak_t* pak = &file.paks[0];
/* list */
ctrpak_note_info_t notes[CTRPAK_MAX_NOTES];
size_t count = ctrpak_pak_list_notes(pak, notes);
for (size_t i = 0; i < count; i++) {
char full[CTRPAK_FULLNAME_BUF_SIZE];
ctrpak_note_full_name(¬es[i].note, full, sizeof(full));
printf("%2zu %-20s %3zu blocks\n", notes[i].index, full, notes[i].pages);
}
/* read a note's data (caller-supplied buffer; always a multiple of 256 bytes) */
uint8_t data[32 * 1024];
size_t size = 0;
ctrpak_pak_read_note(pak, 0, data, sizeof(data), &size);
/* create a note */
uint8_t save[1500];
memset(save, 0xA5, sizeof(save));
ctrpak_note_t meta = ctrpak_note_make("MARIO KART 64", "NKTE", "01");
size_t slot;
err = ctrpak_pak_write_note(pak, &meta, save, sizeof(save), &slot);
if (err != CTRPAK_OK) fprintf(stderr, "%s\n", ctrpak_error_string(err)); /* NOT_ENOUGH_SPACE, ... */
/* delete, rename, copy between paks */
ctrpak_pak_delete_note(pak, 1, false);
ctrpak_pak_rename_note(pak, 0, "KART", "GP", true);
ctrpak_pak_copy_note(pak, 0, &file.paks[1], NULL);
/* integrity */
ctrpak_validation_report_t r = ctrpak_pak_validate(pak);
if (!r.ok) {
ctrpak_repair_report_t rep = ctrpak_pak_repair(pak);
char buf[4096];
ctrpak_repair_report_to_string(&rep, buf, sizeof(buf));
puts(buf);
}
ctrpak_pakfile_save_default(&file, "mario.mpk"); /* keeps the source format */
ctrpak_pakfile_save(&file, "mario.n64", CTRPAK_FORMAT_DEXDRIVE); /* or convert */
ctrpak_pakfile_free(&file);| Type | What it's for |
|---|---|
ctrpak_pak_t |
a 32 KiB image and its whole file system (fixed struct, no allocation) |
ctrpak_pakfile_t |
the file on disk: 1..8 paks (heap allocated) + DexDrive comments |
ctrpak_note_t / ctrpak_note_info_t |
a 32 byte note table entry + size/position |
ctrpak_notefile_meta_t |
metadata of a note exported to its own file |
ctrpak_validation_report_t / ctrpak_repair_report_t |
diagnostics and what repair did |
ctrpak_error_t |
the return code of every fallible function |
ctrpak_charset_* |
ASCII <-> pak charset conversion |
Main functions taking a ctrpak_pak_t*:
ctrpak_pak_list_notes ctrpak_pak_get_note ctrpak_pak_note_info
ctrpak_pak_find_note ctrpak_pak_read_note ctrpak_pak_write_note
ctrpak_pak_update_note ctrpak_pak_delete_note ctrpak_pak_rename_note
ctrpak_pak_set_note_metadata ctrpak_pak_copy_note ctrpak_pak_free_pages
ctrpak_pak_used_pages ctrpak_pak_note_pages ctrpak_pak_max_note_size
ctrpak_pak_validate ctrpak_pak_repair ctrpak_pak_defragment
ctrpak_pak_format ctrpak_pak_refresh_checksums ctrpak_pak_inode
ctrpak_pak_set_inode ctrpak_pak_chain_pages ctrpak_pak_id_block
ctrpak_pak_label
ctrpak info file.mpk # container, paks, free space, serial
ctrpak list file.mpk [--slot N]
ctrpak read file.mpk ZELDA.A -o zelda.note [--raw]
ctrpak write file.mpk zelda.note [--name NAME.EXT] [--game-code NZLE]
ctrpak delete file.mpk 0 [--erase]
ctrpak rename file.mpk 0 NEW.NAME
ctrpak copy source.mpk 0 dest.mpk
ctrpak format new.mpk [--slots 4]
ctrpak check file.mpk
ctrpak repair file.mpk [-o out.mpk]
ctrpak defrag file.mpk
ctrpak convert input.mpk output.n64 --to dexdrive
ctrpak dump file.mpk 0
<note> accepts either the slot index (0-15) or the note name. Commands that
modify a pak write back to the same file, or to -o when given.
| Format | Size | Detection |
|---|---|---|
Raw (.mpk, .pak, .sav, .bin) |
32,768 | exact size |
| Multi-pak (mupen64plus) | N x 32,768 (up to 8) | multiple of the pak size |
DexDrive (.n64) |
4,160 + 32,768 | 123-456-STD signature |
Standalone note (.note) |
32 + N x 256 | plausible note header |
Headerless payloads of any size are also accepted by ctrpak_notefile_*
(CTRPAK_NOTE_LAYOUT_DATA_ONLY) and zero-padded up to whole blocks.
32 KiB = 128 pages ("blocks") of 256 bytes:
| Page | Content |
|---|---|
| 0 | ID area: label (32 B) + 4 copies of the ID block at 0x20, 0x60, 0x80, 0xC0 |
| 1 | index table (inode), 128 big endian 16-bit entries |
| 2 | index table backup |
| 3-4 | note table: 16 entries of 32 bytes |
| 5-127 | data: 123 usable blocks |
- ID block:
checksum= sum of the first 14 big endian words;inverse= sum of their complements (equivalently0xFFF2 - checksum). - Index table: entry
1= end of chain,3= free block,5..127= next block. Byte 1 of the page holds the sum of the low bytes of entries 5..127 (0x71on a freshly formatted pak). - Note entry: game code (4), company code (2), start block (2), status (bit 1 = occupied), reserved, data sum, extension (4) and name (16), the last two in the N64 charset.
Byte-by-byte details in docs/FORMAT.md.
- Reads use the primary index table; if its checksum is broken and the backup's is good, the library reads through the backup (the same thing the SDK itself does). Any write resyncs both before changing anything.
- Cyclic, truncated, or free-block-terminated chains return
CTRPAK_ERR_BROKEN_CHAINinstead of returning garbage. ctrpak_pak_repair()restores ID block copies, resyncs the index tables, drops unreadable notes, resolves shared blocks in favor of the older note, and returns orphan blocks to the free pool - reporting every action taken.ctrpak_validation_report_tholds up toCTRPAK_MAX_ISSUES(24) detailed issues; beyond that,truncatedflags when a badly corrupted pak had more problems than the list could hold. The aggregate counters (note_count,used_pages,free_pages,orphan_pages, the ID/inode booleans) always reflect the whole pak, even when truncated.
- The character conversion table and usage semantics (16 notes per pak,
1 block = 256 bytes, copy/delete/repair) are based on
Controller-Pak-Manager
by manfriedn64, which uses the N64 SDK's official (closed)
osPfs/nuContPakAPI. - The on-disk layout follows the official SDK's PFS (
__OSPackId,__OSInode,__OSDir), also documented by the N64 homebrew community. - The hardware bridge uses the
<mempak.h>/<joypad.h>API from libdragon, the open source N64 homebrew SDK.
MIT - see LICENSE.