Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

libctrpak

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

Features

  • 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, no malloc. Only ctrpak_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)

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.

Build

Without cmake:

make            # library, CLI, tests and example in build/
make test       # runs the 153 checks
sudo make install PREFIX=/usr/local

With cmake:

cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build

In another cmake project:

add_subdirectory(libctrpak)
target_link_libraries(my_app PRIVATE ctrpak::ctrpak)

Quick start

#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(&notes[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);

API

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

CLI

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.

Supported file formats

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.

The format, in short

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 (equivalently 0xFFF2 - 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 (0x71 on 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.

Behavior on damaged paks

  • 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_CHAIN instead 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_t holds up to CTRPAK_MAX_ISSUES (24) detailed issues; beyond that, truncated flags 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.

Credits

  • 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/nuContPak API.
  • 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.

License

MIT - see LICENSE.

About

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.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages