Merge remote-tracking branch 'networkfusion/ed64-basic' into ed64-basic

This commit is contained in:
ariahiro64 2023-10-10 17:21:17 -04:00
commit 0ba36f59d4
51 changed files with 2530 additions and 1292 deletions

View File

@ -6,9 +6,9 @@ RUN apt-get update && \
wget https://github.com/DragonMinded/libdragon/releases/download/toolchain-continuous-prerelease/gcc-toolchain-mips64-x86_64.deb && \
dpkg -i gcc-toolchain-mips64-x86_64.deb && \
rm gcc-toolchain-mips64-x86_64.deb && \
wget https://github.com/Polprzewodnikowy/SummerCart64/releases/download/v2.16.0/sc64-deployer-linux-v2.16.0.tar.gz && \
tar -xf sc64-deployer-linux-v2.16.0.tar.gz -C /usr/local/bin && \
rm sc64-deployer-linux-v2.16.0.tar.gz && \
wget https://github.com/Polprzewodnikowy/SummerCart64/releases/download/v2.17.0/sc64-deployer-linux-v2.17.0.tar.gz && \
tar -xf sc64-deployer-linux-v2.17.0.tar.gz -C /usr/local/bin && \
rm sc64-deployer-linux-v2.17.0.tar.gz && \
git config --global --add safe.directory "*" && \
SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" && \
echo "$SNIPPET" >> "/root/.bashrc"

View File

@ -14,6 +14,7 @@ N64_CFLAGS += -iquote $(SOURCE_DIR) -iquote $(ASSETS_DIR) -I $(SOURCE_DIR)/libs
SRCS = \
main.c \
boot/boot.c \
boot/cic.c \
boot/crc32.c \
boot/reboot.S \
flashcart/64drive/64drive_ll.c \
@ -24,7 +25,6 @@ SRCS = \
flashcart/sc64/sc64.c \
flashcart/ed64/ed64_ll.c \
flashcart/ed64/ed64.c \
hdmi/hdmi.c \
libs/libspng/spng/spng.c \
libs/mini.c/src/mini.c \
libs/miniz/miniz_tdef.c \
@ -38,12 +38,14 @@ SRCS = \
menu/components/common.c \
menu/components/context_menu.c \
menu/components/file_list.c \
menu/disk_info.c \
menu/fonts.c \
menu/hdmi.c \
menu/menu.c \
menu/mp3_player.c \
menu/path.c \
menu/png_decoder.c \
menu/rom_database.c \
menu/rom_info.c \
menu/settings.c \
menu/sound.c \
menu/usb_comm.c \
@ -53,6 +55,7 @@ SRCS = \
menu/views/fault.c \
menu/views/file_info.c \
menu/views/image_viewer.c \
menu/views/load_disk.c \
menu/views/load_emulator.c \
menu/views/load_rom.c \
menu/views/music_player.c \

View File

@ -12,6 +12,7 @@ An open source menu for N64 flashcarts.
## Current (notable) menu features
* Fully Open Source.
* Loads all known N64 games (including iQue and Aleck64 ROMs (even if they are byteswapped)).
* Fully emulates the 64DD and loads 64DD disks (SummerCart64 only).
* Emulator support (NES, SNES, GB, GBC) ROMs.
* N64 ROM box image support.
* Background image (PNG) support.
@ -31,6 +32,13 @@ Download the latest `menu.bin` file from the releases page, then put it in the r
Ensure the cart has the latest [firmware](https://github.com/Polprzewodnikowy/SummerCart64/releases/latest) installed.
Download the latest `sc64menu.n64` file from the releases page, then put it in the root directory of your SD card.
#### 64DD disk support
For the ability to load and run 64DD disk images, you need to add the folder `/menu/64ddipl` on the SD card.
Download and add the relevant ipl files and rename them before adding them to the folder:
- `NDDE0.n64` the US Prototype IPL can be downloaded from [here](https://64dd.org/dumps/64DD_IPL_US_MJR.n64)
- `NDXJ0.n64` the JPN Development IPL can be downloaded from [here](https://64dd.org/dumps/64DD_IPL_DEV_H4G.n64)
- `NDDJ2.n64` the JPN Retail IPL can be downloaded from [here](https://64dd.org/dumps/N64DD_IPLROM_(J).zip)
### ED64
Download the `ED64.v64` ROM from the latest action run assets and place it in the `/ED64` folder.
@ -38,7 +46,7 @@ Download the `ED64.v64` ROM from the latest action run assets and place it in th
Download the `ED64P.v64` ROM from the latest action run assets and place it in the `/ED64P` folder.
### Common to all
### Common to all flashcarts
#### ROM Boxart
To use boxart, you need to place png files of size 158x112 in the folder `/menu/boxart` on the SD card.
@ -48,7 +56,7 @@ i.e. for GoldenEye 3 letters, this would be `NGE.png`.
A known set of PNG files using 2 letter ID's can be downloaded [here](https://mega.nz/file/6cNGwSqI#8X5ukb65n3YMlGaUtSOGXkKo9HxVnnMOgqn94Epcr7w).
#### Emulator support
Emulators should be added to the `/emulators` directory on the SD card.
Emulators should be added to the `/menu/emulators` directory on the SD card.
Menu currently supports the following emulators and associated ROM file names:
- **NES**: [neon64v2](https://github.com/hcs64/neon64v2) by *hcs64* - `neon64bu.rom`
@ -65,11 +73,11 @@ You can use a dev container in VSCode to ease development.
## To deploy:
### SC64
* Download the deployer [here](https://github.com/Polprzewodnikowy/SummerCart64/releases/download/v2.16.0/sc64-deployer-windows-v2.16.0.zip)
* Download the deployer [here](https://github.com/Polprzewodnikowy/SummerCart64/releases/download/v2.17.0/sc64-deployer-windows-v2.17.0.zip)
* Extract and place `sc64deployer.exe` in the `tools/sc64` directory.
Make sure that your firmware is compatible (currently v2.16.0+)
See: https://github.com/Polprzewodnikowy/SummerCart64/blob/v2.16.0/docs/00_quick_startup_guide.md#firmware-backupupdate
Make sure that your firmware is compatible (currently v2.17.0+)
See: https://github.com/Polprzewodnikowy/SummerCart64/blob/v2.17.0/docs/00_quick_startup_guide.md#firmware-backupupdate
#### From the devcontainer
@ -84,7 +92,6 @@ Then in the dev container, use `make run` or `make run-debug`
* Run `./localdeploy.bat` from the terminal
Toggle the N64 power switch to load the ROM.
`ms-vscode.makefile-tools` will help (installed automatically in dev container).
@ -94,6 +101,11 @@ The ROM can be found in the `output` directory.
NOTE: a "release" version of the SC64 menu is called `sc64menu.n64` and can be created for when you want to add it directly to the SDCard. This is generated by running `make all` or running `make sc64`.
### Others
* Add the required file to the correct folder on your SD card.
# Update Libdragon submodule
This repo currently uses the `unstable` branch as a submodule at a specific commit.
To update to the latest version, use `git submodule update --remote ` from the terminal.

@ -1 +1 @@
Subproject commit 352679922bad5403f8ea4b9123dfe0403d030bc2
Subproject commit dd2202a6082cd3d92f4c22e64b27f148612c4c3a

View File

@ -1,8 +1,8 @@
#include <libdragon.h>
#include "boot.h"
#include "boot_io.h"
#include "crc32.h"
#include "boot.h"
#include "cic.h"
#define C0_STATUS_FR (1 << 26)
@ -14,61 +14,26 @@ extern uint32_t reboot_start __attribute__((section(".text")));
extern size_t reboot_size __attribute__((section(".text")));
typedef struct {
const uint32_t crc32;
const uint8_t seed;
} ipl3_crc32_t;
static const ipl3_crc32_t ipl3_crc32[] = {
{ .crc32 = 0x587BD543, .seed = 0xAC }, // 5101
{ .crc32 = 0x6170A4A1, .seed = 0x3F }, // 6101
{ .crc32 = 0x009E9EA3, .seed = 0x3F }, // 7102
{ .crc32 = 0x90BB6CB5, .seed = 0x3F }, // 6102/7101
{ .crc32 = 0x0B050EE0, .seed = 0x78 }, // x103
{ .crc32 = 0x98BC2C86, .seed = 0x91 }, // x105
{ .crc32 = 0xACC8580A, .seed = 0x85 }, // x106
{ .crc32 = 0x0E018159, .seed = 0xDD }, // 5167
{ .crc32 = 0x10C68B18, .seed = 0xDD }, // NDXJ0
{ .crc32 = 0xBC605D0A, .seed = 0xDD }, // NDDJ0
{ .crc32 = 0x502C4466, .seed = 0xDD }, // NDDJ1
{ .crc32 = 0x0C965795, .seed = 0xDD }, // NDDJ2
{ .crc32 = 0x8FEBA21E, .seed = 0xDE }, // NDDE0
};
static io32_t *boot_get_device_base (boot_params_t *params) {
io32_t *device_base_address = ROM_CART;
if (params->device_type == BOOT_DEVICE_TYPE_DD) {
if (params->device_type == BOOT_DEVICE_TYPE_64DD) {
device_base_address = ROM_DDIPL;
}
return device_base_address;
}
static bool boot_detect_cic_seed (boot_params_t *params) {
static void boot_detect_cic_seed (boot_params_t *params) {
io32_t *base = boot_get_device_base(params);
uint32_t ipl3[1008] __attribute__((aligned(8)));
uint8_t ipl3[IPL3_LENGTH] __attribute__((aligned(8)));
data_cache_hit_writeback_invalidate(ipl3, sizeof(ipl3));
dma_read_raw_async(ipl3, (uint32_t) (&base[16]), sizeof(ipl3));
dma_wait();
uint32_t crc32 = crc32_calculate(ipl3, sizeof(ipl3));
for (int i = 0; i < sizeof(ipl3_crc32) / sizeof(ipl3_crc32_t); i++) {
if (ipl3_crc32[i].crc32 == crc32) {
params->cic_seed = ipl3_crc32[i].seed;
return true;
}
params->cic_seed = cic_get_seed(cic_detect(ipl3));
}
return false;
}
bool boot_is_warm (void) {
return (OS_INFO->reset_type == OS_INFO_RESET_TYPE_NMI);
}
void boot (boot_params_t *params) {
if (params->tv_type == BOOT_TV_TYPE_PASSTHROUGH) {
@ -76,9 +41,7 @@ void boot (boot_params_t *params) {
}
if (params->detect_cic_seed) {
if (!boot_detect_cic_seed(params)) {
params->cic_seed = 0x3F;
}
boot_detect_cic_seed(params);
}
OS_INFO->mem_size_6105 = OS_INFO->mem_size;
@ -157,7 +120,7 @@ void boot (boot_params_t *params) {
boot_device = (params->device_type & 0x01);
tv_type = (params->tv_type & 0x03);
reset_type = BOOT_RESET_TYPE_COLD;
reset_type = BOOT_RESET_TYPE_NMI;
cic_seed = (params->cic_seed & 0xFF);
version = (params->tv_type == BOOT_TV_TYPE_PAL) ? 6
: (params->tv_type == BOOT_TV_TYPE_NTSC) ? 1

View File

@ -15,7 +15,7 @@
/** @brief Boot device type enumeration */
typedef enum {
BOOT_DEVICE_TYPE_ROM = 0,
BOOT_DEVICE_TYPE_DD = 1,
BOOT_DEVICE_TYPE_64DD = 1,
} boot_device_type_t;
/** @brief Reset type enumeration */

56
src/boot/cic.c Normal file
View File

@ -0,0 +1,56 @@
#include "cic.h"
#include "crc32.h"
typedef struct {
const uint32_t crc32;
const cic_type_t type;
} ipl3_crc32_t;
static const ipl3_crc32_t ipl3_crc32[] = {
{ .crc32 = 0x587BD543, .type = CIC_5101 },
{ .crc32 = 0x0E018159, .type = CIC_5167 },
{ .crc32 = 0x6170A4A1, .type = CIC_6101 },
{ .crc32 = 0x009E9EA3, .type = CIC_7102 },
{ .crc32 = 0x90BB6CB5, .type = CIC_6102_7101 },
{ .crc32 = 0x0B050EE0, .type = CIC_x103 },
{ .crc32 = 0x98BC2C86, .type = CIC_x105 },
{ .crc32 = 0xACC8580A, .type = CIC_x106 },
{ .crc32 = 0xBC605D0A, .type = CIC_8301 },
{ .crc32 = 0x502C4466, .type = CIC_8302 },
{ .crc32 = 0x0C965795, .type = CIC_8303 },
{ .crc32 = 0x10C68B18, .type = CIC_8401 },
{ .crc32 = 0x8FEBA21E, .type = CIC_8501 },
};
cic_type_t cic_detect (uint8_t *ipl3) {
uint32_t crc32 = crc32_calculate(ipl3, IPL3_LENGTH);
for (int i = 0; i < sizeof(ipl3_crc32) / sizeof(ipl3_crc32_t); i++) {
if (ipl3_crc32[i].crc32 == crc32) {
return ipl3_crc32[i].type;
}
}
return CIC_UNKNOWN;
}
uint8_t cic_get_seed (cic_type_t cic_type) {
switch (cic_type) {
case CIC_5101: return 0xAC;
case CIC_5167: return 0xDD;
case CIC_6101: return 0x3F;
case CIC_7102: return 0x3F;
case CIC_6102_7101: return 0x3F;
case CIC_x103: return 0x78;
case CIC_x105: return 0x91;
case CIC_x106: return 0x85;
case CIC_8301: return 0xDD;
case CIC_8302: return 0xDD;
case CIC_8303: return 0xDD;
case CIC_8401: return 0xDD;
case CIC_8501: return 0xDE;
default: return 0x3F;
}
}

33
src/boot/cic.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef CIC_H__
#define CIC_H__
#include <stdint.h>
#define IPL3_LENGTH (4032)
typedef enum {
CIC_5101,
CIC_5167,
CIC_6101,
CIC_7102,
CIC_6102_7101,
CIC_x103,
CIC_x105,
CIC_x106,
CIC_8301,
CIC_8302,
CIC_8303,
CIC_8401,
CIC_8501,
CIC_UNKNOWN,
} cic_type_t;
cic_type_t cic_detect (uint8_t *ipl3);
uint8_t cic_get_seed (cic_type_t cic_type);
#endif

View File

@ -2,20 +2,6 @@
#define REBOOT_ADDRESS 0xA4001000
#define STACK_ADDRESS 0xA4001FF0
#define RI_ADDRESS 0xA4700000
#define RI_MODE 0x00
#define RI_CONFIG 0x04
#define RI_CURRENT_LOAD 0x08
#define RI_SELECT 0x0C
#define RI_REFRESH 0x10
#define RI_LATENCY 0x14
#define RI_MODE_RESET 0x00000000
#define RI_MODE_STANDBY 0x0000000E
#define RDRAM_RESET_DELAY 1024
#define RDRAM_STANDBY_DELAY 512
.set noat
.section .text.reboot, "ax", %progbits
@ -43,39 +29,24 @@ reboot_entry:
li $sp, STACK_ADDRESS
bnez $s5, reset_rdram_skip
detect_console_region:
li $t0, 1
beq $s4, $zero, pal_console
beq $s4, $t0, ntsc_console
b mpal_console
reset_rdram:
li $t0, RI_ADDRESS
pal_console:
li $ra, 0xA4001554
b reset_registers
li $t1, RI_MODE_RESET
sw $t1, RI_MODE($t0)
ntsc_console:
li $ra, 0xA4001550
b reset_registers
li $t2, RDRAM_RESET_DELAY
1:
addiu $t2, (-1)
bnez $t2, 1b
sw $zero, RI_CONFIG($t0)
sw $zero, RI_CURRENT_LOAD($t0)
sw $zero, RI_SELECT($t0)
sw $zero, RI_REFRESH($t0)
li $t1, RI_MODE_STANDBY
sw $t1, RI_MODE($t0)
li $t2, RDRAM_STANDBY_DELAY
1:
addiu $t2, (-1)
bnez $t2, 1b
reset_rdram_skip:
prepare_registers:
la $t0, ra_table
sll $t1, $s4, 2
add $t0, $t1
lw $ra, ($t0)
mpal_console:
li $ra, 0xA4001554
reset_registers:
move $at, $zero
move $v0, $zero
move $v1, $zero
@ -98,17 +69,11 @@ prepare_registers:
move $k0, $zero
move $k1, $zero
move $gp, $zero
move $s8, $zero
move $fp, $zero
run_ipl3:
li $t3, IPL3_ENTRY
jr $t3
ra_values:
.set ra_table, REBOOT_ADDRESS + (. - reboot_start)
.word 0xA4001554
.word 0xA4001550
.word 0xA4001554
.set reboot_size, (. - reboot_start)
.global reboot_size

View File

@ -25,57 +25,64 @@ static d64_device_variant_t device_variant = DEVICE_VARIANT_UNKNOWN;
static d64_save_type_t current_save_type = SAVE_TYPE_NONE;
static flashcart_error_t d64_init (void) {
static flashcart_err_t d64_init (void) {
uint16_t fpga_revision;
uint32_t bootloader_version;
if (d64_ll_enable_extended_mode(false)) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
if (d64_ll_get_version(&device_variant, &fpga_revision, &bootloader_version)) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
if (fpga_revision < SUPPORTED_FPGA_REVISION) {
return FLASHCART_ERROR_OUTDATED;
return FLASHCART_ERR_OUTDATED;
}
if (d64_ll_enable_save_writeback(false)) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
if (d64_ll_set_save_type(SAVE_TYPE_NONE)) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
current_save_type = SAVE_TYPE_NONE;
if (d64_ll_enable_cartrom_writes(true)) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
if (d64_ll_set_persistent_variable_storage(false, 0, 0)) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
return FLASHCART_OK;
}
static flashcart_error_t d64_deinit (void) {
static flashcart_err_t d64_deinit (void) {
if (d64_ll_enable_cartrom_writes(false)) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
return FLASHCART_OK;
}
static flashcart_error_t d64_load_rom (char *rom_path, flashcart_progress_callback_t *progress) {
static bool d64_has_feature (flashcart_features_t feature) {
switch (feature) {
case FLASHCART_FEATURE_64DD: return false;
default: return false;
}
}
static flashcart_err_t d64_load_rom (char *rom_path, flashcart_progress_callback_t *progress) {
FIL fil;
UINT br;
if (f_open(&fil, strip_sd_prefix(rom_path), FA_READ) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
fix_file_size(&fil);
@ -84,7 +91,7 @@ static flashcart_error_t d64_load_rom (char *rom_path, flashcart_progress_callba
if (rom_size > MiB(64)) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
size_t sdram_size = MiB(64);
@ -94,7 +101,7 @@ static flashcart_error_t d64_load_rom (char *rom_path, flashcart_progress_callba
size_t block_size = MIN(sdram_size - offset, chunk_size);
if (f_read(&fil, (void *) (ROM_ADDRESS + offset), block_size, &br) != FR_OK) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (progress) {
progress(f_tell(&fil) / (float) (f_size(&fil)));
@ -102,22 +109,22 @@ static flashcart_error_t d64_load_rom (char *rom_path, flashcart_progress_callba
}
if (f_tell(&fil) != rom_size) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (f_close(&fil) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
return FLASHCART_OK;
}
static flashcart_error_t d64_load_file (char *file_path, uint32_t rom_offset, uint32_t file_offset) {
static flashcart_err_t d64_load_file (char *file_path, uint32_t rom_offset, uint32_t file_offset) {
FIL fil;
UINT br;
if (f_open(&fil, strip_sd_prefix(file_path), FA_READ) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
fix_file_size(&fil);
@ -126,37 +133,37 @@ static flashcart_error_t d64_load_file (char *file_path, uint32_t rom_offset, ui
if (file_size > (MiB(64) - rom_offset)) {
f_close(&fil);
return FLASHCART_ERROR_ARGS;
return FLASHCART_ERR_ARGS;
}
if (f_lseek(&fil, file_offset) != FR_OK) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (f_read(&fil, (void *) (ROM_ADDRESS + rom_offset), file_size, &br) != FR_OK) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (br != file_size) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (f_close(&fil) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
return FLASHCART_OK;
}
static flashcart_error_t d64_load_save (char *save_path) {
static flashcart_err_t d64_load_save (char *save_path) {
uint8_t eeprom_contents[2048] __attribute__((aligned(8)));
FIL fil;
UINT br;
if (f_open(&fil, strip_sd_prefix(save_path), FA_READ) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
size_t save_size = f_size(&fil);
@ -175,28 +182,28 @@ static flashcart_error_t d64_load_save (char *save_path) {
if (f_read(&fil, address, save_size, &br) != FR_OK) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (is_eeprom_save) {
if (d64_ll_write_eeprom_contents(eeprom_contents)) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
}
if (f_close(&fil) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (br != save_size) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
return FLASHCART_OK;
}
static flashcart_error_t d64_set_save_type (flashcart_save_type_t save_type) {
static flashcart_err_t d64_set_save_type (flashcart_save_type_t save_type) {
d64_save_type_t type;
switch (save_type) {
@ -226,15 +233,15 @@ static flashcart_error_t d64_set_save_type (flashcart_save_type_t save_type) {
type = (device_variant == DEVICE_VARIANT_A) ? SAVE_TYPE_FLASHRAM_PKST2 : SAVE_TYPE_FLASHRAM;
break;
default:
return FLASHCART_ERROR_ARGS;
return FLASHCART_ERR_ARGS;
}
if (d64_ll_enable_save_writeback(false)) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
if (d64_ll_set_save_type(type)) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
current_save_type = type;
@ -242,13 +249,13 @@ static flashcart_error_t d64_set_save_type (flashcart_save_type_t save_type) {
return FLASHCART_OK;
}
static flashcart_error_t d64_set_save_writeback (uint32_t *sectors) {
static flashcart_err_t d64_set_save_writeback (uint32_t *sectors) {
if (d64_ll_write_save_writeback_lba_list(sectors)) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
if (d64_ll_enable_save_writeback(true)) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
return FLASHCART_OK;
@ -258,9 +265,12 @@ static flashcart_error_t d64_set_save_writeback (uint32_t *sectors) {
static flashcart_t flashcart_d64 = {
.init = d64_init,
.deinit = d64_deinit,
.has_feature = d64_has_feature,
.load_rom = d64_load_rom,
.load_file = d64_load_file,
.load_save = d64_load_save,
.load_64dd_ipl = NULL,
.load_64dd_disk = NULL,
.set_save_type = d64_set_save_type,
.set_save_writeback = d64_set_save_writeback,
};

View File

@ -15,7 +15,7 @@
extern int ed_exit(void);
static flashcart_error_t ed64_init (void) {
static flashcart_err_t ed64_init (void) {
// TODO: partly already done, see https://github.com/DragonMinded/libdragon/blob/4ec469d26b6dc4e308caf3d5b86c2b340b708bbd/src/libcart/cart.c#L1064
@ -25,7 +25,7 @@ static flashcart_error_t ed64_init (void) {
return FLASHCART_OK;
}
static flashcart_error_t ed64_deinit (void) {
static flashcart_err_t ed64_deinit (void) {
// For the moment, just use libCart exit.
ed_exit();
@ -33,25 +33,31 @@ static flashcart_error_t ed64_deinit (void) {
return FLASHCART_OK;
}
static flashcart_error_t ed64_load_rom (char *rom_path, flashcart_progress_callback_t *progress) {
static bool ed64_has_feature (flashcart_features_t feature) {
switch (feature) {
case FLASHCART_FEATURE_64DD: return false;
default: return false;
}
}
static flashcart_err_t ed64_load_rom (char *rom_path, flashcart_progress_callback_t *progress) {
FIL fil;
UINT br;
if (f_open(&fil, strip_sd_prefix(rom_path), FA_READ) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
fix_file_size(&fil);
size_t rom_size = f_size(&fil);
// FIXME: if the cart is not V3 or X5 or X7, we need to - 128KiB
if (rom_size > MiB(64) - KiB(128)) {
// FIXME: if the cart is not V3 or X5 or X7, we need probably need to - 128KiB
if (rom_size > MiB(64)) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
size_t sdram_size = rom_size;
size_t chunk_size = MiB(1);
@ -59,7 +65,7 @@ static flashcart_error_t ed64_load_rom (char *rom_path, flashcart_progress_callb
size_t block_size = MIN(sdram_size - offset, chunk_size);
if (f_read(&fil, (void *) (ROM_ADDRESS + offset), block_size, &br) != FR_OK) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (progress) {
progress(f_tell(&fil) / (float) (f_size(&fil)));
@ -67,23 +73,23 @@ static flashcart_error_t ed64_load_rom (char *rom_path, flashcart_progress_callb
}
if (f_tell(&fil) != sdram_size) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (f_close(&fil) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
return FLASHCART_OK;
}
static flashcart_error_t ed64_load_file (char *file_path, uint32_t rom_offset, uint32_t file_offset) {
static flashcart_err_t ed64_load_file (char *file_path, uint32_t rom_offset, uint32_t file_offset) {
FIL fil;
UINT br;
if (f_open(&fil, strip_sd_prefix(file_path), FA_READ) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
fix_file_size(&fil);
@ -93,31 +99,31 @@ static flashcart_error_t ed64_load_file (char *file_path, uint32_t rom_offset, u
// FIXME: if the cart is not V3 or X5 or X7, we need to - 128KiB
if (file_size > (MiB(64) - KiB(128) - rom_offset)) {
f_close(&fil);
return FLASHCART_ERROR_ARGS;
return FLASHCART_ERR_ARGS;
}
if (f_lseek(&fil, file_offset) != FR_OK) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (f_read(&fil, (void *) (ROM_ADDRESS + rom_offset), file_size, &br) != FR_OK) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (br != file_size) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (f_close(&fil) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
return FLASHCART_OK;
}
static flashcart_error_t ed64_load_save (char *save_path) {
static flashcart_err_t ed64_load_save (char *save_path) {
void *address = NULL;
ed64_save_type_t type = ed64_ll_get_save_type();
@ -131,35 +137,35 @@ static flashcart_error_t ed64_load_save (char *save_path) {
break;
case SAVE_TYPE_NONE:
default:
return FLASHCART_ERROR_ARGS;
return FLASHCART_ERR_ARGS;
}
FIL fil;
UINT br;
if (f_open(&fil, strip_sd_prefix(save_path), FA_READ) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
size_t save_size = f_size(&fil);
if (f_read(&fil, address, save_size, &br) != FR_OK) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (f_close(&fil) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (br != save_size) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
return FLASHCART_OK;
}
static flashcart_error_t ed64_set_save_type (flashcart_save_type_t save_type) {
static flashcart_err_t ed64_set_save_type (flashcart_save_type_t save_type) {
ed64_save_type_t type;
switch (save_type) {
@ -184,7 +190,7 @@ static flashcart_error_t ed64_set_save_type (flashcart_save_type_t save_type) {
type = SAVE_TYPE_FLASHRAM;
break;
default:
return FLASHCART_ERROR_ARGS;
return FLASHCART_ERR_ARGS;
}
ed64_ll_set_save_type(type);
@ -196,9 +202,12 @@ static flashcart_error_t ed64_set_save_type (flashcart_save_type_t save_type) {
static flashcart_t flashcart_ed64 = {
.init = ed64_init,
.deinit = ed64_deinit,
.has_feature = ed64_has_feature,
.load_rom = ed64_load_rom,
.load_file = ed64_load_file,
.load_save = ed64_load_save,
.load_64dd_ipl = NULL,
.load_64dd_disk = NULL,
.set_save_type = ed64_set_save_type,
.set_save_writeback = NULL,
};

View File

@ -31,7 +31,21 @@ static const size_t SAVE_SIZE[__FLASHCART_SAVE_TYPE_END] = {
static uint32_t save_writeback_sectors[SAVE_WRITEBACK_MAX_SECTORS] __attribute__((aligned(8)));
static flashcart_error_t dummy_init (void) {
static void save_writeback_sectors_callback (uint32_t sector_count, uint32_t file_sector, uint32_t cluster_sector, uint32_t cluster_size) {
for (uint32_t i = 0; i < cluster_size; i++) {
uint32_t offset = file_sector + i;
uint32_t sector = cluster_sector + i;
if ((offset > SAVE_WRITEBACK_MAX_SECTORS) || (offset > sector_count)) {
return;
}
save_writeback_sectors[offset] = sector;
}
}
static flashcart_err_t dummy_init (void) {
return FLASHCART_OK;
}
@ -53,52 +67,60 @@ static flashcart_t *flashcart = &((flashcart_t) {
#endif
flashcart_error_t flashcart_init (void) {
flashcart_error_t error;
// NOTE: Explicitly support only these flashcarts in this specific initialization order.
struct {
int type;
int (* libcart_init) (void);
flashcart_t *(* get) (void);
} flashcarts[CART_MAX] = {
{ CART_CI, ci_init, d64_get_flashcart }, // 64drive
{ CART_SC, sc_init, sc64_get_flashcart }, // SC64
{ CART_EDX, edx_init, NULL }, // Series X EverDrive-64
{ CART_ED, ed_init, ed64_get_flashcart }, // Original EverDrive-64
};
for (int i = 0; i < CART_MAX; i++) {
if (flashcarts[i].libcart_init() >= 0) {
cart_type = flashcarts[i].type;
if (flashcarts[i].get) {
flashcart = flashcarts[i].get();
}
break;
char *flashcart_convert_error_message (flashcart_err_t err) {
switch (err) {
case FLASHCART_OK: return "No error";
case FLASHCART_ERR_NOT_DETECTED: return "No flashcart hardware was detected";
case FLASHCART_ERR_OUTDATED: return "Outdated flashcart firmware";
case FLASHCART_ERR_SD_CARD: return "Error during SD card initialization";
case FLASHCART_ERR_ARGS: return "Invalid argument passed to flashcart function";
case FLASHCART_ERR_LOAD: return "Error during loading data into flashcart";
case FLASHCART_ERR_INT: return "Internal flashcart error";
case FLASHCART_ERR_FUNCTION_NOT_SUPPORTED: return "Flashcart doesn't support this function";
default: return "Unknown flashcart error";
}
}
if (cart_type == CART_NULL) {
return FLASHCART_ERROR_NOT_DETECTED;
}
flashcart_err_t flashcart_init (void) {
flashcart_err_t err;
bool sd_card_initialized = debug_init_sdfs("sd:/", -1);
#ifndef NDEBUG
// NOTE: Some flashcarts doesn't have USB port, can't throw error here
debug_init_usblog();
#endif
if ((error = flashcart->init()) != FLASHCART_OK) {
return error;
switch (cart_type) {
case CART_CI: // 64drive
flashcart = d64_get_flashcart();
break;
case CART_EDX: // Series X EverDrive-64
case CART_ED: // Original EverDrive-64
flashcart = ed64_get_flashcart();
break;
case CART_SC: // SummerCart64
flashcart = sc64_get_flashcart();
break;
default:
return FLASHCART_ERR_NOT_DETECTED;
}
if (!debug_init_sdfs("sd:/", -1)) {
return FLASHCART_ERROR_SD_CARD;
if ((err = flashcart->init()) != FLASHCART_OK) {
return err;
}
if (!sd_card_initialized) {
return FLASHCART_ERR_SD_CARD;
}
return FLASHCART_OK;
}
flashcart_error_t flashcart_deinit (void) {
flashcart_err_t flashcart_deinit (void) {
if (flashcart->deinit) {
return flashcart->deinit();
}
@ -106,54 +128,41 @@ flashcart_error_t flashcart_deinit (void) {
return FLASHCART_OK;
}
flashcart_error_t flashcart_load_rom (char *rom_path, bool byte_swap, flashcart_progress_callback_t *progress) {
flashcart_error_t error;
bool flashcart_has_feature (flashcart_features_t feature) {
return flashcart->has_feature(feature);
}
if ((rom_path == NULL) || (!file_exists(rom_path)) || (file_get_size(rom_path) < KiB(4))) {
return FLASHCART_ERROR_ARGS;
flashcart_err_t flashcart_load_rom (char *rom_path, bool byte_swap, flashcart_progress_callback_t *progress) {
flashcart_err_t err;
if (rom_path == NULL) {
return FLASHCART_ERR_ARGS;
}
cart_card_byteswap = byte_swap;
error = flashcart->load_rom(rom_path, progress);
err = flashcart->load_rom(rom_path, progress);
cart_card_byteswap = false;
return error;
return err;
}
flashcart_error_t flashcart_load_file (char *file_path, uint32_t rom_offset, uint32_t file_offset) {
if ((file_path == NULL) || (!file_exists(file_path))) {
return FLASHCART_ERROR_ARGS;
}
if ((file_offset % FS_SECTOR_SIZE) != 0) {
return FLASHCART_ERROR_ARGS;
flashcart_err_t flashcart_load_file (char *file_path, uint32_t rom_offset, uint32_t file_offset) {
if ((file_path == NULL) || ((file_offset % FS_SECTOR_SIZE) != 0)) {
return FLASHCART_ERR_ARGS;
}
return flashcart->load_file(file_path, rom_offset, file_offset);
}
static void save_writeback_sectors_callback (uint32_t sector_count, uint32_t file_sector, uint32_t cluster_sector, uint32_t cluster_size) {
for (uint32_t i = 0; i < cluster_size; i++) {
uint32_t offset = file_sector + i;
uint32_t sector = cluster_sector + i;
if ((offset > SAVE_WRITEBACK_MAX_SECTORS) || (offset > sector_count)) {
return;
}
save_writeback_sectors[offset] = sector;
}
}
flashcart_error_t flashcart_load_save (char *save_path, flashcart_save_type_t save_type) {
flashcart_error_t error;
flashcart_err_t flashcart_load_save (char *save_path, flashcart_save_type_t save_type) {
flashcart_err_t err;
if (save_type >= __FLASHCART_SAVE_TYPE_END) {
return FLASHCART_ERROR_ARGS;
return FLASHCART_ERR_ARGS;
}
if ((error = flashcart->set_save_type(save_type)) != FLASHCART_OK) {
return error;
if ((err = flashcart->set_save_type(save_type)) != FLASHCART_OK) {
return err;
}
if ((save_path == NULL) || (save_type == FLASHCART_SAVE_TYPE_NONE)) {
@ -162,19 +171,19 @@ flashcart_error_t flashcart_load_save (char *save_path, flashcart_save_type_t sa
if (!file_exists(save_path)) {
if (file_allocate(save_path, SAVE_SIZE[save_type])) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (file_fill(save_path, 0xFF)) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
}
if (file_get_size(save_path) != SAVE_SIZE[save_type]) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if ((error = flashcart->load_save(save_path)) != FLASHCART_OK) {
return error;
if ((err = flashcart->load_save(save_path)) != FLASHCART_OK) {
return err;
}
if (flashcart->set_save_writeback) {
@ -182,12 +191,36 @@ flashcart_error_t flashcart_load_save (char *save_path, flashcart_save_type_t sa
save_writeback_sectors[i] = 0;
}
if (file_get_sectors(save_path, save_writeback_sectors_callback)) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if ((error = flashcart->set_save_writeback(save_writeback_sectors)) != FLASHCART_OK) {
return error;
if ((err = flashcart->set_save_writeback(save_writeback_sectors)) != FLASHCART_OK) {
return err;
}
}
return FLASHCART_OK;
}
flashcart_err_t flashcart_load_64dd_ipl (char *ipl_path, flashcart_progress_callback_t *progress) {
if (!flashcart->load_64dd_ipl) {
return FLASHCART_ERR_FUNCTION_NOT_SUPPORTED;
}
if (ipl_path == NULL) {
return FLASHCART_ERR_ARGS;
}
return flashcart->load_64dd_ipl(ipl_path, progress);
}
flashcart_err_t flashcart_load_64dd_disk (char *disk_path, flashcart_disk_parameters_t *disk_parameters) {
if (!flashcart->load_64dd_disk) {
return FLASHCART_ERR_FUNCTION_NOT_SUPPORTED;
}
if ((disk_path == NULL) || (disk_parameters == NULL)) {
return FLASHCART_ERR_ARGS;
}
return flashcart->load_64dd_disk(disk_path, disk_parameters);
}

View File

@ -15,13 +15,19 @@
/** @brief Flashcart error enumeration */
typedef enum {
FLASHCART_OK,
FLASHCART_ERROR_NOT_DETECTED,
FLASHCART_ERROR_OUTDATED,
FLASHCART_ERROR_SD_CARD,
FLASHCART_ERROR_ARGS,
FLASHCART_ERROR_LOAD,
FLASHCART_ERROR_INT,
} flashcart_error_t;
FLASHCART_ERR_NOT_DETECTED,
FLASHCART_ERR_OUTDATED,
FLASHCART_ERR_SD_CARD,
FLASHCART_ERR_ARGS,
FLASHCART_ERR_LOAD,
FLASHCART_ERR_INT,
FLASHCART_ERR_FUNCTION_NOT_SUPPORTED,
} flashcart_err_t;
/** @brief List of optional supported flashcart features */
typedef enum {
FLASHCART_FEATURE_64DD,
} flashcart_features_t;
/** @brief Flashcart save type enumeration */
typedef enum {
@ -36,25 +42,39 @@ typedef enum {
__FLASHCART_SAVE_TYPE_END
} flashcart_save_type_t;
typedef struct {
bool development_drive;
uint8_t disk_type;
bool bad_system_area_lbas[24];
uint8_t defect_tracks[16][12];
} flashcart_disk_parameters_t;
typedef void flashcart_progress_callback_t (float progress);
/** @brief Flashcart Structure */
typedef struct {
flashcart_error_t (*init) (void);
flashcart_error_t (*deinit) (void);
flashcart_error_t (*load_rom) (char *rom_path, flashcart_progress_callback_t *progress);
flashcart_error_t (*load_file) (char *file_path, uint32_t rom_offset, uint32_t file_offset);
flashcart_error_t (*load_save) (char *save_path);
flashcart_error_t (*set_save_type) (flashcart_save_type_t save_type);
flashcart_error_t (*set_save_writeback) (uint32_t *sectors);
flashcart_err_t (*init) (void);
flashcart_err_t (*deinit) (void);
bool (*has_feature) (flashcart_features_t feature);
flashcart_err_t (*load_rom) (char *rom_path, flashcart_progress_callback_t *progress);
flashcart_err_t (*load_file) (char *file_path, uint32_t rom_offset, uint32_t file_offset);
flashcart_err_t (*load_save) (char *save_path);
flashcart_err_t (*load_64dd_ipl) (char *ipl_path, flashcart_progress_callback_t *progress);
flashcart_err_t (*load_64dd_disk) (char *disk_path, flashcart_disk_parameters_t *disk_parameters);
flashcart_err_t (*set_save_type) (flashcart_save_type_t save_type);
flashcart_err_t (*set_save_writeback) (uint32_t *sectors);
} flashcart_t;
flashcart_error_t flashcart_init (void);
flashcart_error_t flashcart_deinit (void);
flashcart_error_t flashcart_load_rom (char *rom_path, bool byte_swap, flashcart_progress_callback_t *progress);
flashcart_error_t flashcart_load_file (char *file_path, uint32_t rom_offset, uint32_t file_offset);
flashcart_error_t flashcart_load_save (char *save_path, flashcart_save_type_t save_type);
char *flashcart_convert_error_message (flashcart_err_t err);
flashcart_err_t flashcart_init (void);
flashcart_err_t flashcart_deinit (void);
bool flashcart_has_feature (flashcart_features_t feature);
flashcart_err_t flashcart_load_rom (char *rom_path, bool byte_swap, flashcart_progress_callback_t *progress);
flashcart_err_t flashcart_load_file (char *file_path, uint32_t rom_offset, uint32_t file_offset);
flashcart_err_t flashcart_load_save (char *save_path, flashcart_save_type_t save_type);
flashcart_err_t flashcart_load_64dd_ipl (char *ipl_path, flashcart_progress_callback_t *progress);
flashcart_err_t flashcart_load_64dd_disk (char *disk_path, flashcart_disk_parameters_t *disk_parameters);
#endif

View File

@ -15,35 +15,89 @@
#define SRAM_FLASHRAM_ADDRESS (0x08000000)
#define ROM_ADDRESS (0x10000000)
#define IPL_ADDRESS (0x13BC0000)
#define EXTENDED_ADDRESS (0x14000000)
#define SHADOW_ADDRESS (0x1FFC0000)
#define EEPROM_ADDRESS (0x1FFE2000)
#define SUPPORTED_MAJOR_VERSION (2)
#define SUPPORTED_MINOR_VERSION (16)
#define SUPPORTED_MINOR_VERSION (17)
#define SUPPORTED_REVISION (0)
#define DISK_MAPPING_ROM_OFFSET (0x02000000)
#define DISK_MAX_SECTORS (126820)
static flashcart_error_t load_to_flash (FIL *fil, void *address, size_t size, UINT *br, flashcart_progress_callback_t *progress) {
#define DISK_TRACKS (1175)
#define DISK_HEADS (2)
#define DISK_BLOCKS (2)
#define DISK_SECTORS_PER_BLOCK (85)
#define DISK_ZONES (16)
#define DISK_BAD_TRACKS_PER_ZONE (12)
#define DISK_TYPES (7)
#define DISK_SYSTEM_LBA_COUNT (24)
#define THB_UNMAPPED (0xFFFFFFFF)
#define THB_WRITABLE_FLAG (1 << 31)
static const struct {
uint8_t head;
uint8_t sector_length;
uint8_t tracks;
uint16_t track_offset;
} zone_mapping[DISK_ZONES] = {
{ 0, 232, 158, 0 },
{ 0, 216, 158, 158 },
{ 0, 208, 149, 316 },
{ 0, 192, 149, 465 },
{ 0, 176, 149, 614 },
{ 0, 160, 149, 763 },
{ 0, 144, 149, 912 },
{ 0, 128, 114, 1061 },
{ 1, 216, 158, 0 },
{ 1, 208, 158, 158 },
{ 1, 192, 149, 316 },
{ 1, 176, 149, 465 },
{ 1, 160, 149, 614 },
{ 1, 144, 149, 763 },
{ 1, 128, 149, 912 },
{ 1, 112, 114, 1061 },
};
static const uint8_t vzone_to_pzone[DISK_TYPES][DISK_ZONES] = {
{0, 1, 2, 9, 8, 3, 4, 5, 6, 7, 15, 14, 13, 12, 11, 10},
{0, 1, 2, 3, 10, 9, 8, 4, 5, 6, 7, 15, 14, 13, 12, 11},
{0, 1, 2, 3, 4, 11, 10, 9, 8, 5, 6, 7, 15, 14, 13, 12},
{0, 1, 2, 3, 4, 5, 12, 11, 10, 9, 8, 6, 7, 15, 14, 13},
{0, 1, 2, 3, 4, 5, 6, 13, 12, 11, 10, 9, 8, 7, 15, 14},
{0, 1, 2, 3, 4, 5, 6, 7, 14, 13, 12, 11, 10, 9, 8, 15},
{0, 1, 2, 3, 4, 5, 6, 7, 15, 14, 13, 12, 11, 10, 9, 8},
};
static const uint8_t rom_zones[DISK_TYPES] = { 5, 7, 9, 11, 13, 15, 16 };
static uint32_t disk_sectors_start_offset;
static flashcart_err_t load_to_flash (FIL *fil, void *address, size_t size, UINT *br, flashcart_progress_callback_t *progress) {
size_t erase_block_size;
UINT bp;
*br = 0;
if (sc64_ll_flash_get_erase_block_size(&erase_block_size) != SC64_OK) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
while (size > 0) {
size_t program_size = MIN(size, erase_block_size);
if (sc64_ll_flash_erase_block(address) != SC64_OK) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
if (f_read(fil, address, program_size, &bp) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (sc64_ll_flash_wait_busy() != SC64_OK) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
if (progress) {
progress(f_tell(fil) / (float) (f_size(fil)));
@ -56,27 +110,113 @@ static flashcart_error_t load_to_flash (FIL *fil, void *address, size_t size, UI
return FLASHCART_OK;
}
static flashcart_error_t sc64_init (void) {
static uint32_t disk_sectors_start (uint32_t offset) {
disk_sectors_start_offset = offset;
return (offset + (DISK_MAX_SECTORS * sizeof(uint32_t)));
}
static void disk_sectors_callback (uint32_t sector_count, uint32_t file_sector, uint32_t cluster_sector, uint32_t cluster_size) {
for (uint32_t i = 0; i < cluster_size; i++) {
uint32_t offset = file_sector + i;
uint32_t sector = cluster_sector + i;
if ((offset > DISK_MAX_SECTORS) || (offset > sector_count)) {
return;
}
io_write(ROM_ADDRESS + disk_sectors_start_offset + (offset * sizeof(uint32_t)), sector);
}
}
static bool disk_zone_track_is_bad (uint8_t zone, uint8_t track, flashcart_disk_parameters_t *disk_parameters) {
for (int i = 0; i < DISK_BAD_TRACKS_PER_ZONE; i++) {
if (disk_parameters->defect_tracks[zone][i] == track) {
return true;
}
}
return false;
}
static bool disk_system_lba_is_bad (uint16_t lba, flashcart_disk_parameters_t *disk_parameters) {
if (lba < DISK_SYSTEM_LBA_COUNT) {
return disk_parameters->bad_system_area_lbas[lba];
}
return false;
}
static void disk_set_thb_mapping (uint32_t offset, uint16_t track, uint8_t head, uint8_t block, bool valid, bool writable, int file_offset) {
uint32_t index = (track << 2) | (head << 1) | (block);
uint32_t mapping = valid ? ((writable ? THB_WRITABLE_FLAG : 0) | (file_offset & ~(THB_WRITABLE_FLAG))) : THB_UNMAPPED;
io_write(ROM_ADDRESS + offset + (index * sizeof(uint32_t)), mapping);
}
static uint32_t disk_load_thb_table (uint32_t offset, flashcart_disk_parameters_t *disk_parameters) {
int file_offset = 0;
uint16_t lba = 0;
uint8_t starting_block = 0;
for (uint8_t vzone = 0; vzone < DISK_ZONES; vzone++) {
uint8_t pzone = vzone_to_pzone[disk_parameters->disk_type][vzone];
uint8_t head = zone_mapping[pzone].head;
uint8_t sector_length = zone_mapping[pzone].sector_length;
uint8_t tracks = zone_mapping[pzone].tracks;
uint16_t track_offset = zone_mapping[pzone].track_offset;
bool reverse = (head != 0);
int zone_track_start = (reverse ? (tracks - 1) : 0);
int zone_track_end = (reverse ? (-1) : tracks);
for (int zone_track = zone_track_start; zone_track != zone_track_end; zone_track += (reverse ? (-1) : 1)) {
uint16_t track = track_offset + zone_track;
if (disk_zone_track_is_bad(pzone, zone_track, disk_parameters)) {
disk_set_thb_mapping(offset, track, head, 0, false, false, 0);
disk_set_thb_mapping(offset, track, head, 1, false, false, 0);
continue;
}
for (uint8_t block = 0; block < DISK_BLOCKS; block += 1) {
bool valid = !(disk_system_lba_is_bad(lba, disk_parameters));
bool writable = (vzone >= rom_zones[disk_parameters->disk_type]);
disk_set_thb_mapping(offset, track, head, (starting_block ^ block), valid, writable, file_offset);
file_offset += (sector_length * DISK_SECTORS_PER_BLOCK);
lba += 1;
}
starting_block ^= 1;
}
}
return (offset + (DISK_TRACKS * DISK_HEADS * DISK_BLOCKS * sizeof(uint32_t)));
}
static flashcart_err_t sc64_init (void) {
uint16_t major;
uint16_t minor;
uint32_t revision;
if (sc64_ll_get_version(&major, &minor, &revision) != SC64_OK) {
return FLASHCART_ERROR_OUTDATED;
return FLASHCART_ERR_OUTDATED;
}
if (major != SUPPORTED_MAJOR_VERSION) {
return FLASHCART_ERROR_OUTDATED;
return FLASHCART_ERR_OUTDATED;
}
if (minor < SUPPORTED_MINOR_VERSION) {
return FLASHCART_ERROR_OUTDATED;
return FLASHCART_ERR_OUTDATED;
} else if (minor == SUPPORTED_MINOR_VERSION && revision < SUPPORTED_REVISION) {
return FLASHCART_ERROR_OUTDATED;
return FLASHCART_ERR_OUTDATED;
}
bool writeback_pending;
do {
if (sc64_ll_writeback_pending(&writeback_pending) != SC64_OK) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
} while (writeback_pending);
@ -102,14 +242,14 @@ static flashcart_error_t sc64_init (void) {
for (int i = 0; i < sizeof(default_config) / sizeof(default_config[0]); i++) {
if (sc64_ll_set_config(default_config[i].id, default_config[i].value) != SC64_OK) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
}
return FLASHCART_OK;
}
static flashcart_error_t sc64_deinit (void) {
static flashcart_err_t sc64_deinit (void) {
sc64_ll_set_config(CFG_ID_ROM_WRITE_ENABLE, false);
sc64_ll_lock();
@ -117,12 +257,19 @@ static flashcart_error_t sc64_deinit (void) {
return FLASHCART_OK;
}
static flashcart_error_t sc64_load_rom (char *rom_path, flashcart_progress_callback_t *progress) {
static bool sc64_has_feature (flashcart_features_t feature) {
switch (feature) {
case FLASHCART_FEATURE_64DD: return true;
default: return false;
}
}
static flashcart_err_t sc64_load_rom (char *rom_path, flashcart_progress_callback_t *progress) {
FIL fil;
UINT br;
if (f_open(&fil, strip_sd_prefix(rom_path), FA_READ) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
fix_file_size(&fil);
@ -131,7 +278,7 @@ static flashcart_error_t sc64_load_rom (char *rom_path, flashcart_progress_callb
if (rom_size > MiB(78)) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
bool shadow_enabled = (rom_size > (MiB(64) - KiB(128)));
@ -146,7 +293,7 @@ static flashcart_error_t sc64_load_rom (char *rom_path, flashcart_progress_callb
size_t block_size = MIN(sdram_size - offset, chunk_size);
if (f_read(&fil, (void *) (ROM_ADDRESS + offset), block_size, &br) != FR_OK) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (progress) {
progress(f_tell(&fil) / (float) (f_size(&fil)));
@ -154,56 +301,56 @@ static flashcart_error_t sc64_load_rom (char *rom_path, flashcart_progress_callb
}
if (f_tell(&fil) != sdram_size) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (sc64_ll_set_config(CFG_ID_ROM_SHADOW_ENABLE, shadow_enabled) != SC64_OK) {
f_close(&fil);
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
if (shadow_enabled) {
flashcart_error_t error = load_to_flash(&fil, (void *) (SHADOW_ADDRESS), shadow_size, &br, progress);
if (error != FLASHCART_OK) {
flashcart_err_t err = load_to_flash(&fil, (void *) (SHADOW_ADDRESS), shadow_size, &br, progress);
if (err != FLASHCART_OK) {
f_close(&fil);
return error;
return err;
}
if (br != shadow_size) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
}
if (sc64_ll_set_config(CFG_ID_ROM_EXTENDED_ENABLE, extended_enabled) != SC64_OK) {
f_close(&fil);
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
if (extended_enabled) {
flashcart_error_t error = load_to_flash(&fil, (void *) (EXTENDED_ADDRESS), extended_size, &br, progress);
if (error != FLASHCART_OK) {
flashcart_err_t err = load_to_flash(&fil, (void *) (EXTENDED_ADDRESS), extended_size, &br, progress);
if (err != FLASHCART_OK) {
f_close(&fil);
return error;
return err;
}
if (br != extended_size) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
}
if (f_close(&fil) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
return FLASHCART_OK;
}
static flashcart_error_t sc64_load_file (char *file_path, uint32_t rom_offset, uint32_t file_offset) {
static flashcart_err_t sc64_load_file (char *file_path, uint32_t rom_offset, uint32_t file_offset) {
FIL fil;
UINT br;
if (f_open(&fil, strip_sd_prefix(file_path), FA_READ) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
fix_file_size(&fil);
@ -212,36 +359,36 @@ static flashcart_error_t sc64_load_file (char *file_path, uint32_t rom_offset, u
if (file_size > (MiB(64) - rom_offset)) {
f_close(&fil);
return FLASHCART_ERROR_ARGS;
return FLASHCART_ERR_ARGS;
}
if (f_lseek(&fil, file_offset) != FR_OK) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (f_read(&fil, (void *) (ROM_ADDRESS + rom_offset), file_size, &br) != FR_OK) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (br != file_size) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (f_close(&fil) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
return FLASHCART_OK;
}
static flashcart_error_t sc64_load_save (char *save_path) {
static flashcart_err_t sc64_load_save (char *save_path) {
void *address = NULL;
uint32_t value;
if (sc64_ll_get_config(CFG_ID_SAVE_TYPE, &value) != SC64_OK) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
sc64_save_type_t type = (sc64_save_type_t) (value);
@ -258,35 +405,120 @@ static flashcart_error_t sc64_load_save (char *save_path) {
break;
case SAVE_TYPE_NONE:
default:
return FLASHCART_ERROR_ARGS;
return FLASHCART_ERR_ARGS;
}
FIL fil;
UINT br;
if (f_open(&fil, strip_sd_prefix(save_path), FA_READ) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
size_t save_size = f_size(&fil);
if (f_read(&fil, address, save_size, &br) != FR_OK) {
f_close(&fil);
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (f_close(&fil) != FR_OK) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
if (br != save_size) {
return FLASHCART_ERROR_LOAD;
return FLASHCART_ERR_LOAD;
}
return FLASHCART_OK;
}
static flashcart_error_t sc64_set_save_type (flashcart_save_type_t save_type) {
static flashcart_err_t sc64_load_64dd_ipl (char *ipl_path, flashcart_progress_callback_t *progress) {
FIL fil;
UINT br;
if (f_open(&fil, strip_sd_prefix(ipl_path), FA_READ) != FR_OK) {
return FLASHCART_ERR_LOAD;
}
fix_file_size(&fil);
size_t ipl_size = f_size(&fil);
if (ipl_size > MiB(4)) {
f_close(&fil);
return FLASHCART_ERR_LOAD;
}
size_t chunk_size = KiB(256);
for (int offset = 0; offset < ipl_size; offset += chunk_size) {
size_t block_size = MIN(ipl_size - offset, chunk_size);
if (f_read(&fil, (void *) (IPL_ADDRESS + offset), block_size, &br) != FR_OK) {
f_close(&fil);
return FLASHCART_ERR_LOAD;
}
if (progress) {
progress(f_tell(&fil) / (float) (f_size(&fil)));
}
}
if (f_tell(&fil) != ipl_size) {
f_close(&fil);
return FLASHCART_ERR_LOAD;
}
if (f_close(&fil) != FR_OK) {
return FLASHCART_ERR_LOAD;
}
return FLASHCART_OK;
}
static flashcart_err_t sc64_load_64dd_disk (char *disk_path, flashcart_disk_parameters_t *disk_parameters) {
sc64_disk_mapping_t mapping = { .count = 0 };
uint32_t next_mapping_offset = DISK_MAPPING_ROM_OFFSET;
// TODO: Support loading multiple disks
// LOOP START
mapping.disks[mapping.count].thb_table = next_mapping_offset;
mapping.disks[mapping.count].sector_table = disk_load_thb_table(mapping.disks[mapping.count].thb_table, disk_parameters);
next_mapping_offset = disk_sectors_start(mapping.disks[mapping.count].sector_table);
if (file_get_sectors(disk_path, disk_sectors_callback)) {
return FLASHCART_ERR_LOAD;
}
mapping.count += 1;
// LOOP END
if (mapping.count == 0) {
return FLASHCART_ERR_ARGS;
}
if (sc64_ll_set_disk_mapping(&mapping) != SC64_OK) {
return FLASHCART_ERR_INT;
}
sc64_drive_type_t drive_type = disk_parameters->development_drive ? DRIVE_TYPE_DEVELOPMENT : DRIVE_TYPE_RETAIL;
const struct {
sc64_cfg_id_t id;
uint32_t value;
} config[] = {
{ CFG_ID_DD_MODE, DD_MODE_FULL },
{ CFG_ID_DD_SD_ENABLE, true },
{ CFG_ID_DD_DRIVE_TYPE, drive_type },
{ CFG_ID_DD_DISK_STATE, DISK_STATE_INSERTED },
{ CFG_ID_BUTTON_MODE, BUTTON_MODE_DD_DISK_SWAP },
};
for (int i = 0; i < sizeof(config) / sizeof(config[0]); i++) {
if (sc64_ll_set_config(config[i].id, config[i].value) != SC64_OK) {
return FLASHCART_ERR_INT;
}
}
return FLASHCART_OK;
}
static flashcart_err_t sc64_set_save_type (flashcart_save_type_t save_type) {
sc64_save_type_t type;
switch (save_type) {
@ -315,21 +547,21 @@ static flashcart_error_t sc64_set_save_type (flashcart_save_type_t save_type) {
type = SAVE_TYPE_FLASHRAM;
break;
default:
return FLASHCART_ERROR_ARGS;
return FLASHCART_ERR_ARGS;
}
if (sc64_ll_set_config(CFG_ID_SAVE_TYPE, type) != SC64_OK) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
return FLASHCART_OK;
}
static flashcart_error_t sc64_set_save_writeback (uint32_t *sectors) {
static flashcart_err_t sc64_set_save_writeback (uint32_t *sectors) {
pi_dma_write_data(sectors, SC64_BUFFERS->BUFFER, 1024);
if (sc64_ll_writeback_enable(SC64_BUFFERS->BUFFER) != SC64_OK) {
return FLASHCART_ERROR_INT;
return FLASHCART_ERR_INT;
}
return FLASHCART_OK;
@ -339,9 +571,12 @@ static flashcart_error_t sc64_set_save_writeback (uint32_t *sectors) {
static flashcart_t flashcart_sc64 = {
.init = sc64_init,
.deinit = sc64_deinit,
.has_feature = sc64_has_feature,
.load_rom = sc64_load_rom,
.load_file = sc64_load_file,
.load_save = sc64_load_save,
.load_64dd_ipl = sc64_load_64dd_ipl,
.load_64dd_disk = sc64_load_64dd_disk,
.set_save_type = sc64_set_save_type,
.set_save_writeback = sc64_set_save_writeback,
};

View File

@ -1,5 +1,6 @@
#include <libdragon.h>
#include "../flashcart_utils.h"
#include "sc64_ll.h"
@ -23,6 +24,7 @@ typedef enum {
CMD_ID_VERSION_GET = 'V',
CMD_ID_CONFIG_GET = 'c',
CMD_ID_CONFIG_SET = 'C',
CMD_ID_DISK_MAPPING_SET = 'D',
CMD_ID_WRITEBACK_PENDING = 'w',
CMD_ID_WRITEBACK_SD_INFO = 'W',
CMD_ID_FLASH_WAIT_BUSY = 'p',
@ -91,6 +93,32 @@ sc64_error_t sc64_ll_set_config (sc64_cfg_id_t id, uint32_t value) {
return sc64_ll_execute_cmd(&cmd);
}
sc64_error_t sc64_ll_set_disk_mapping (sc64_disk_mapping_t *disk_mapping) {
int disk_count = disk_mapping->count;
if (disk_count <= 0 || disk_count > 4) {
return SC64_ERROR_BAD_ARGUMENT;
}
uint32_t info[8] __attribute__((aligned(8)));
for (int i = 0; i < disk_count; i++) {
info[i * 2] = disk_mapping->disks[i].thb_table;
info[(i * 2) + 1] = disk_mapping->disks[i].sector_table;
}
uint32_t length = (disk_mapping->count * 2 * sizeof(uint32_t));
pi_dma_write_data(info, SC64_BUFFERS->BUFFER, length);
sc64_cmd_t cmd = {
.id = CMD_ID_DISK_MAPPING_SET,
.arg = { (uint32_t) (SC64_BUFFERS->BUFFER), length }
};
return sc64_ll_execute_cmd(&cmd);
}
sc64_error_t sc64_ll_writeback_pending (bool *pending) {
sc64_cmd_t cmd = {
.id = CMD_ID_WRITEBACK_PENDING

View File

@ -110,11 +110,20 @@ typedef enum {
BUTTON_MODE_DD_DISK_SWAP,
} sc64_button_mode_t;
typedef struct {
int count;
struct {
uint32_t thb_table;
uint32_t sector_table;
} disks[4];
} sc64_disk_mapping_t;
void sc64_ll_lock (void);
sc64_error_t sc64_ll_get_version (uint16_t *major, uint16_t *minor, uint32_t *revision);
sc64_error_t sc64_ll_get_config (sc64_cfg_id_t cfg, uint32_t *value);
sc64_error_t sc64_ll_set_config (sc64_cfg_id_t cfg, uint32_t value);
sc64_error_t sc64_ll_set_disk_mapping (sc64_disk_mapping_t *disk_mapping);
sc64_error_t sc64_ll_writeback_pending (bool *pending);
sc64_error_t sc64_ll_writeback_enable (void *address);
sc64_error_t sc64_ll_flash_wait_busy (void);

View File

@ -1,61 +0,0 @@
// Implementation based on https://gitlab.com/pixelfx-public/n64-game-id
#include <libdragon.h>
#include "hdmi.h"
#define ROM_ADDRESS (0x10000000)
#define DDIPL_ADDRESS (0x06000000)
#define CONTROLLER_PORT (0)
#define GAME_ID_COMMAND (0x1D)
void hdmi_reset_game_id (void) {
uint8_t joybus_tx[10];
uint8_t joybus_rx[1];
memset(joybus_tx, 0, sizeof(joybus_tx));
execute_raw_command(
CONTROLLER_PORT,
GAME_ID_COMMAND,
sizeof(joybus_tx),
sizeof(joybus_rx),
joybus_tx,
joybus_rx
);
}
void hdmi_broadcast_game_id (boot_params_t *boot_params) {
uint8_t rom_header[0x40] __attribute__((aligned(8)));
uint32_t pi_address = ROM_ADDRESS;
if (boot_params->device_type == BOOT_DEVICE_TYPE_DD) {
pi_address = DDIPL_ADDRESS;
}
dma_read_async(rom_header, pi_address, sizeof(rom_header));
dma_wait();
uint8_t joybus_tx[10];
uint8_t joybus_rx[1];
// Copy CRC
memcpy(joybus_tx, rom_header + 0x10, 8);
// Copy media format
joybus_tx[8] = rom_header[0x3B];
// Copy country code
joybus_tx[9] = rom_header[0x3E];
execute_raw_command(
CONTROLLER_PORT,
GAME_ID_COMMAND,
sizeof(joybus_tx),
sizeof(joybus_rx),
joybus_tx,
joybus_rx
);
}

View File

@ -1,12 +0,0 @@
#ifndef HDMI_H__
#define HDMI_H__
#include "boot/boot.h"
void hdmi_reset_game_id (void);
void hdmi_broadcast_game_id (boot_params_t *boot_params);
#endif

View File

@ -1,19 +1,14 @@
#include <libdragon.h>
#include "boot/boot.h"
#include "hdmi/hdmi.h"
#include "menu/menu.h"
int main (void) {
boot_params_t boot_params;
hdmi_reset_game_id();
menu_run(&boot_params);
hdmi_broadcast_game_id(&boot_params);
disable_interrupts();
boot(&boot_params);

View File

@ -3,8 +3,11 @@
#include "actions.h"
#define ACTIONS_REPEAT_DELAY 10
#define JOYSTICK_DEADZONE 32
#define ACTIONS_REPEAT_DELAY (8)
static int dir_repeat_delay;
static joypad_8way_t last_dir = JOYPAD_8WAY_NONE;
static void actions_clear (menu_t *menu) {
@ -12,7 +15,8 @@ static void actions_clear (menu_t *menu) {
menu->actions.go_down = false;
menu->actions.go_left = false;
menu->actions.go_right = false;
menu->actions.fast = false;
menu->actions.go_fast = false;
menu->actions.enter = false;
menu->actions.back = false;
menu->actions.options = false;
@ -20,93 +24,84 @@ static void actions_clear (menu_t *menu) {
menu->actions.settings = false;
}
static void actions_update_direction (menu_t *menu) {
joypad_8way_t held_dir = joypad_get_direction(JOYPAD_PORT_1, JOYPAD_2D_DPAD | JOYPAD_2D_STICK);
joypad_8way_t fast_dir = joypad_get_direction(JOYPAD_PORT_1, JOYPAD_2D_C);
void actions_update (menu_t *menu) {
controller_scan();
struct controller_data down = get_keys_down();
struct controller_data held = get_keys_held();
struct controller_data pressed = get_keys_pressed();
if (down.c[0].err != ERROR_NONE) {
return;
if (fast_dir != JOYPAD_8WAY_NONE) {
held_dir = fast_dir;
menu->actions.go_fast = true;
}
actions_clear(menu);
joypad_8way_t final_dir = held_dir;
if (down.c[0].up || down.c[0].C_up) {
menu->actions.go_up = true;
menu->actions.vertical_held_counter = 0;
if (down.c[0].C_up) {
menu->actions.fast = true;
}
} else if (down.c[0].down || down.c[0].C_down) {
menu->actions.go_down = true;
menu->actions.vertical_held_counter = 0;
if (down.c[0].C_down) {
menu->actions.fast = true;
}
} else if (held.c[0].up || held.c[0].C_up) {
menu->actions.vertical_held_counter += 1;
if (menu->actions.vertical_held_counter >= ACTIONS_REPEAT_DELAY) {
menu->actions.go_up = true;
if (held.c[0].C_up) {
menu->actions.fast = true;
}
}
} else if (held.c[0].down || held.c[0].C_down) {
menu->actions.vertical_held_counter += 1;
if (menu->actions.vertical_held_counter >= ACTIONS_REPEAT_DELAY) {
menu->actions.go_down = true;
if (held.c[0].C_down) {
menu->actions.fast = true;
}
}
} else if (pressed.c[0].y > +JOYSTICK_DEADZONE) { // TODO: requires improvement for responsiveness
menu->actions.vertical_held_counter += 1;
if (menu->actions.vertical_held_counter >= ACTIONS_REPEAT_DELAY / 2) {
menu->actions.go_up = true;
if (pressed.c[0].y < +75) {
menu->actions.vertical_held_counter = 0;
}
}
} else if (pressed.c[0].y < -JOYSTICK_DEADZONE) { // TODO: requires improvement for responsiveness
menu->actions.vertical_held_counter += 1;
if (menu->actions.vertical_held_counter >= ACTIONS_REPEAT_DELAY / 2) {
menu->actions.go_down = true;
if (pressed.c[0].y > -75) {
menu->actions.vertical_held_counter = 0;
}
}
if ((last_dir != held_dir) && (last_dir == JOYPAD_8WAY_NONE)) {
dir_repeat_delay = ACTIONS_REPEAT_DELAY;
} else if (dir_repeat_delay > 0) {
final_dir = JOYPAD_8WAY_NONE;
}
if (down.c[0].left) {
menu->actions.go_left = true;
menu->actions.horizontal_held_counter = 0;
} else if (down.c[0].right) {
switch (final_dir) {
case JOYPAD_8WAY_NONE:
break;
case JOYPAD_8WAY_RIGHT:
menu->actions.go_right = true;
menu->actions.horizontal_held_counter = 0;
} else if (held.c[0].left) {
menu->actions.horizontal_held_counter += 1;
if (menu->actions.horizontal_held_counter >= ACTIONS_REPEAT_DELAY) {
menu->actions.go_left = true;
}
} else if (held.c[0].right) {
menu->actions.horizontal_held_counter += 1;
if (menu->actions.horizontal_held_counter >= ACTIONS_REPEAT_DELAY) {
break;
case JOYPAD_8WAY_UP_RIGHT:
menu->actions.go_up = true;
menu->actions.go_right = true;
}
break;
case JOYPAD_8WAY_UP:
menu->actions.go_up = true;
break;
case JOYPAD_8WAY_UP_LEFT:
menu->actions.go_up = true;
menu->actions.go_left = true;
break;
case JOYPAD_8WAY_LEFT:
menu->actions.go_left = true;
break;
case JOYPAD_8WAY_DOWN_LEFT:
menu->actions.go_down = true;
menu->actions.go_left = true;
break;
case JOYPAD_8WAY_DOWN:
menu->actions.go_down = true;
break;
case JOYPAD_8WAY_DOWN_RIGHT:
menu->actions.go_down = true;
menu->actions.go_right = true;
break;
}
if (down.c[0].A) {
if (dir_repeat_delay > 0) {
dir_repeat_delay -= 1;
}
last_dir = held_dir;
}
static void actions_update_buttons (menu_t *menu) {
joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1);
if (pressed.a) {
menu->actions.enter = true;
} else if (down.c[0].B) {
} else if (pressed.b) {
menu->actions.back = true;
} else if (down.c[0].R) {
} else if (pressed.r) {
menu->actions.options = true;
} else if (down.c[0].L) {
} else if (pressed.l) {
menu->actions.system_info = true;
} else if (down.c[0].start) {
} else if (pressed.start) {
menu->actions.settings = true;
}
}
void actions_update (menu_t *menu) {
joypad_poll();
actions_clear(menu);
actions_update_direction(menu);
actions_update_buttons(menu);
}

View File

@ -1,75 +1,97 @@
#include <string.h>
#include <libdragon.h>
#include "cart_load.h"
#include "path.h"
#include "utils/fs.h"
#include "utils/utils.h"
#ifndef SAVES_SUBDIRECTORY
#define SAVES_SUBDIRECTORY "saves"
#define EMU_LOCATION "/emulators"
#endif
#ifndef DDIPL_LOCATION
#define DDIPL_LOCATION "/menu/64ddipl"
#endif
#ifndef EMU_LOCATION
#define EMU_LOCATION "/menu/emulators"
#endif
static bool create_saves_subdirectory (menu_t *menu) {
path_t *save_folder_path = path_clone_push(menu->browser.directory, SAVES_SUBDIRECTORY);
static bool is_64dd_connected (void) {
return (
((io_read(0x05000540) & 0x0000FFFF) == 0x0000) ||
(io_read(0x06001010) == 0x2129FFF8)
);
}
static bool create_saves_subdirectory (path_t *path) {
path_t *save_folder_path = path_clone(path);
path_pop(save_folder_path);
path_push(save_folder_path, SAVES_SUBDIRECTORY);
bool error = directory_create(path_get(save_folder_path));
path_free(save_folder_path);
return error;
}
static flashcart_save_type_t convert_save_type (rom_header_t *header) {
switch (rom_db_match_save_type(*header)) {
case DB_SAVE_TYPE_EEPROM_4K:
return FLASHCART_SAVE_TYPE_EEPROM_4K;
case DB_SAVE_TYPE_EEPROM_16K:
return FLASHCART_SAVE_TYPE_EEPROM_16K;
case DB_SAVE_TYPE_SRAM:
return FLASHCART_SAVE_TYPE_SRAM;
case DB_SAVE_TYPE_SRAM_BANKED:
return FLASHCART_SAVE_TYPE_SRAM_BANKED;
case DB_SAVE_TYPE_SRAM_128K:
return FLASHCART_SAVE_TYPE_SRAM_128K;
case DB_SAVE_TYPE_FLASHRAM:
return FLASHCART_SAVE_TYPE_FLASHRAM;
default:
return FLASHCART_SAVE_TYPE_NONE;
static flashcart_save_type_t convert_save_type (rom_info_t *info) {
switch (info->save_type) {
case SAVE_TYPE_EEPROM_4K: return FLASHCART_SAVE_TYPE_EEPROM_4K;
case SAVE_TYPE_EEPROM_16K: return FLASHCART_SAVE_TYPE_EEPROM_16K;
case SAVE_TYPE_SRAM: return FLASHCART_SAVE_TYPE_SRAM;
case SAVE_TYPE_SRAM_BANKED: return FLASHCART_SAVE_TYPE_SRAM_BANKED;
case SAVE_TYPE_SRAM_128K: return FLASHCART_SAVE_TYPE_SRAM_128K;
case SAVE_TYPE_FLASHRAM: return FLASHCART_SAVE_TYPE_FLASHRAM;
case SAVE_TYPE_FLASHRAM_PKST2: return FLASHCART_SAVE_TYPE_FLASHRAM_PKST2;
default: return FLASHCART_SAVE_TYPE_NONE;
}
}
char *cart_load_convert_error_message (cart_load_err_t err) {
switch (err) {
case CART_LOAD_ERR_SAVES_SUBDIR: return "Could not create saves subdirectory";
case CART_LOAD_ERR_ROM: return "Error during ROM loading";
case CART_LOAD_ERR_SAVE: return "Error during save creation or loading";
case CART_LOAD_ERR_EMU_NOT_FOUND: return "Emulator ROM not found";
case CART_LOAD_ERR_EMU: return "Error during emulator ROM loading";
case CART_LOAD_ERR_EMU_ROM: return "Error during emulated ROM loading";
default: return "Unknown cart load error";
case CART_LOAD_OK: return "Cart load OK";
case CART_LOAD_ERR_ROM_LOAD_FAIL: return "Error occured during ROM loading";
case CART_LOAD_ERR_SAVE_LOAD_FAIL: return "Error occured during save loading";
case CART_LOAD_ERR_64DD_PRESENT: return "64DD accessory is connected to the N64";
case CART_LOAD_ERR_64DD_IPL_NOT_FOUND: return "Required 64DD IPL file was not found";
case CART_LOAD_ERR_64DD_IPL_LOAD_FAIL: return "Error occured during 64DD IPL loading";
case CART_LOAD_ERR_64DD_DISK_LOAD_FAIL: return "Error occured during 64DD disk loading";
case CART_LOAD_ERR_EMU_NOT_FOUND: return "Required emulator file was not found";
case CART_LOAD_ERR_EMU_LOAD_FAIL: return "Error occured during emulator ROM loading";
case CART_LOAD_ERR_EMU_ROM_LOAD_FAIL: return "Error occured during emulated ROM loading";
case CART_LOAD_ERR_CREATE_SAVES_SUBDIR_FAIL: return "Couldn't create saves subdirectory";
case CART_LOAD_ERR_EXP_PAK_NOT_FOUND: return "Mandatory Expansion Pak accessory was not found";
case CART_LOAD_ERR_FUNCTION_NOT_SUPPORTED: return "Your flashcart doesn't support required functionality";
default: return "Unknown error [CART_LOAD]";
}
}
cart_load_err_t cart_load_n64_rom_and_save (menu_t *menu, rom_header_t *header, flashcart_progress_callback_t progress) {
path_t *path = path_clone_push(menu->browser.directory, menu->browser.entry->name);
bool byte_swap = (header->config_flags == ROM_MID_BIG_ENDIAN);
flashcart_save_type_t save_type = convert_save_type(header);
cart_load_err_t cart_load_n64_rom_and_save (menu_t *menu, flashcart_progress_callback_t progress) {
path_t *path = path_clone(menu->load.rom_path);
menu->flashcart_error = flashcart_load_rom(path_get(path), byte_swap, progress);
if (menu->flashcart_error != FLASHCART_OK) {
bool byte_swap = (menu->load.rom_info.endianness == ENDIANNESS_BYTE_SWAP);
flashcart_save_type_t save_type = convert_save_type(&menu->load.rom_info);
menu->flashcart_err = flashcart_load_rom(path_get(path), byte_swap, progress);
if (menu->flashcart_err != FLASHCART_OK) {
path_free(path);
return CART_LOAD_ERR_ROM;
return CART_LOAD_ERR_ROM_LOAD_FAIL;
}
path_ext_replace(path, "sav");
if (menu->settings.use_saves_folder) {
path_push_subdir(path, SAVES_SUBDIRECTORY);
if ((save_type != FLASHCART_SAVE_TYPE_NONE) && create_saves_subdirectory(menu)) {
if ((save_type != FLASHCART_SAVE_TYPE_NONE) && create_saves_subdirectory(path)) {
path_free(path);
return CART_LOAD_ERR_SAVES_SUBDIR;
return CART_LOAD_ERR_CREATE_SAVES_SUBDIR_FAIL;
}
path_push_subdir(path, SAVES_SUBDIRECTORY);
}
menu->flashcart_error = flashcart_load_save(path_get(path), save_type);
if (menu->flashcart_error != FLASHCART_OK) {
menu->flashcart_err = flashcart_load_save(path_get(path), save_type);
if (menu->flashcart_err != FLASHCART_OK) {
path_free(path);
return CART_LOAD_ERR_SAVE;
return CART_LOAD_ERR_SAVE_LOAD_FAIL;
}
path_free(path);
@ -77,8 +99,63 @@ cart_load_err_t cart_load_n64_rom_and_save (menu_t *menu, rom_header_t *header,
return CART_LOAD_OK;
}
cart_load_err_t cart_load_64dd_ipl_and_disk (menu_t *menu, flashcart_progress_callback_t progress) {
if (!flashcart_has_feature(FLASHCART_FEATURE_64DD)) {
return CART_LOAD_ERR_FUNCTION_NOT_SUPPORTED;
}
if (is_64dd_connected()) {
return CART_LOAD_ERR_64DD_PRESENT;
}
if (!is_memory_expanded()) {
return CART_LOAD_ERR_EXP_PAK_NOT_FOUND;
}
path_t *path = path_init("sd:/", DDIPL_LOCATION);
flashcart_disk_parameters_t disk_parameters;
disk_parameters.development_drive = (menu->load.disk_info.region == DISK_REGION_DEVELOPMENT);
disk_parameters.disk_type = menu->load.disk_info.disk_type;
memcpy(disk_parameters.bad_system_area_lbas, menu->load.disk_info.bad_system_area_lbas, sizeof(disk_parameters.bad_system_area_lbas));
memcpy(disk_parameters.defect_tracks, menu->load.disk_info.defect_tracks, sizeof(disk_parameters.defect_tracks));
switch (menu->load.disk_info.region) {
case DISK_REGION_DEVELOPMENT:
path_push(path, "NDXJ0.n64");
break;
case DISK_REGION_JAPANESE:
path_push(path, "NDDJ2.n64");
break;
case DISK_REGION_USA:
path_push(path, "NDDE0.n64");
break;
}
if (!file_exists(path_get(path))) {
path_free(path);
return CART_LOAD_ERR_64DD_IPL_NOT_FOUND;
}
menu->flashcart_err = flashcart_load_64dd_ipl(path_get(path), progress);
if (menu->flashcart_err != FLASHCART_OK) {
path_free(path);
return CART_LOAD_ERR_64DD_IPL_LOAD_FAIL;
}
path_free(path);
menu->flashcart_err = flashcart_load_64dd_disk(path_get(menu->load.disk_path), &disk_parameters);
if (menu->flashcart_err != FLASHCART_OK) {
return CART_LOAD_ERR_64DD_DISK_LOAD_FAIL;
}
return CART_LOAD_OK;
}
cart_load_err_t cart_load_emulator (menu_t *menu, cart_load_emu_type_t emu_type, flashcart_progress_callback_t progress) {
path_t *path = path_init("sd:/", EMU_LOCATION);
flashcart_save_type_t save_type = FLASHCART_SAVE_TYPE_NONE;
uint32_t emulated_rom_offset = 0x200000;
uint32_t emulated_file_offset = 0;
@ -111,10 +188,10 @@ cart_load_err_t cart_load_emulator (menu_t *menu, cart_load_emu_type_t emu_type,
return CART_LOAD_ERR_EMU_NOT_FOUND;
}
menu->flashcart_error = flashcart_load_rom(path_get(path), false, progress);
if (menu->flashcart_error != FLASHCART_OK) {
menu->flashcart_err = flashcart_load_rom(path_get(path), false, progress);
if (menu->flashcart_err != FLASHCART_OK) {
path_free(path);
return CART_LOAD_ERR_EMU;
return CART_LOAD_ERR_EMU_LOAD_FAIL;
}
path_free(path);
@ -123,32 +200,32 @@ cart_load_err_t cart_load_emulator (menu_t *menu, cart_load_emu_type_t emu_type,
switch (emu_type) {
case CART_LOAD_EMU_TYPE_SNES:
// The emulator expects the header to be removed from the ROM being uploaded.
// NOTE: The emulator expects the header to be removed from the ROM being uploaded.
emulated_file_offset = ((file_get_size(path_get(path)) & 0x3FF) == 0x200) ? 0x200 : 0;
break;
default:
break;
}
menu->flashcart_error = flashcart_load_file(path_get(path), emulated_rom_offset, emulated_file_offset);
if (menu->flashcart_error != FLASHCART_OK) {
menu->flashcart_err = flashcart_load_file(path_get(path), emulated_rom_offset, emulated_file_offset);
if (menu->flashcart_err != FLASHCART_OK) {
path_free(path);
return CART_LOAD_ERR_EMU_ROM;
return CART_LOAD_ERR_EMU_ROM_LOAD_FAIL;
}
path_ext_replace(path, "sav");
if (menu->settings.use_saves_folder) {
path_push_subdir(path, SAVES_SUBDIRECTORY);
if ((save_type != FLASHCART_SAVE_TYPE_NONE) && create_saves_subdirectory(menu)) {
if ((save_type != FLASHCART_SAVE_TYPE_NONE) && create_saves_subdirectory(path)) {
path_free(path);
return CART_LOAD_ERR_SAVES_SUBDIR;
return CART_LOAD_ERR_CREATE_SAVES_SUBDIR_FAIL;
}
path_push_subdir(path, SAVES_SUBDIRECTORY);
}
menu->flashcart_error = flashcart_load_save(path_get(path), save_type);
if (menu->flashcart_error != FLASHCART_OK) {
menu->flashcart_err = flashcart_load_save(path_get(path), save_type);
if (menu->flashcart_err != FLASHCART_OK) {
path_free(path);
return CART_LOAD_ERR_SAVE;
return CART_LOAD_ERR_SAVE_LOAD_FAIL;
}
path_free(path);

View File

@ -8,19 +8,26 @@
#define CART_LOAD_H__
#include "disk_info.h"
#include "flashcart/flashcart.h"
#include "menu_state.h"
#include "rom_database.h"
#include "rom_info.h"
typedef enum {
CART_LOAD_OK,
CART_LOAD_ERR_SAVES_SUBDIR,
CART_LOAD_ERR_ROM,
CART_LOAD_ERR_SAVE,
CART_LOAD_ERR_ROM_LOAD_FAIL,
CART_LOAD_ERR_SAVE_LOAD_FAIL,
CART_LOAD_ERR_64DD_PRESENT,
CART_LOAD_ERR_64DD_IPL_NOT_FOUND,
CART_LOAD_ERR_64DD_IPL_LOAD_FAIL,
CART_LOAD_ERR_64DD_DISK_LOAD_FAIL,
CART_LOAD_ERR_EMU_NOT_FOUND,
CART_LOAD_ERR_EMU,
CART_LOAD_ERR_EMU_ROM,
CART_LOAD_ERR_EMU_LOAD_FAIL,
CART_LOAD_ERR_EMU_ROM_LOAD_FAIL,
CART_LOAD_ERR_CREATE_SAVES_SUBDIR_FAIL,
CART_LOAD_ERR_EXP_PAK_NOT_FOUND,
CART_LOAD_ERR_FUNCTION_NOT_SUPPORTED,
} cart_load_err_t;
typedef enum {
@ -33,7 +40,8 @@ typedef enum {
char *cart_load_convert_error_message (cart_load_err_t err);
cart_load_err_t cart_load_n64_rom_and_save (menu_t *menu, rom_header_t *header, flashcart_progress_callback_t progress);
cart_load_err_t cart_load_n64_rom_and_save (menu_t *menu, flashcart_progress_callback_t progress);
cart_load_err_t cart_load_64dd_ipl_and_disk (menu_t *menu, flashcart_progress_callback_t progress);
cart_load_err_t cart_load_emulator (menu_t *menu, cart_load_emu_type_t emu_type, flashcart_progress_callback_t progress);

View File

@ -59,7 +59,7 @@ typedef struct {
surface_t *image;
} component_boxart_t;
component_boxart_t *component_boxart_init (uint8_t media_type, uint16_t id);
component_boxart_t *component_boxart_init (char *game_code);
void component_boxart_free (component_boxart_t *b);
void component_boxart_draw (component_boxart_t *b);

View File

@ -17,24 +17,23 @@ static void png_decoder_callback (png_err_t err, surface_t *decoded_image, void
}
component_boxart_t *component_boxart_init (uint8_t media_type, uint16_t id) {
component_boxart_t *component_boxart_init (char *game_code) {
component_boxart_t *b = calloc(1, sizeof(component_boxart_t));
if (b) {
b->loading = true;
char *path = alloca(strlen(BOXART_DIRECTORY) + 1 + 7 + 1); // allocate for the largest path.
sprintf(path, "%s/%c%c%c.png", BOXART_DIRECTORY, (media_type & 0xFF), ((id >> 8) & 0xFF), (id & 0xFF));
// if the file does not exist, also check for just the id.
if (!file_exists(path)) {
sprintf(path, "%s/%c%c.png", BOXART_DIRECTORY, ((id >> 8) & 0xFF), (id & 0xFF));
}
char *path = alloca(strlen(BOXART_DIRECTORY) + 1 + 7 + 1);
// TODO: This is bad, we should only check for 3 letter codes
sprintf(path, "%s/%.3s.png", BOXART_DIRECTORY, game_code);
if (png_decoder_start(path, BOXART_WIDTH, BOXART_HEIGHT, png_decoder_callback, b) != PNG_OK) {
sprintf(path, "%s/%.2s.png", BOXART_DIRECTORY, &game_code[1]);
if (png_decoder_start(path, BOXART_WIDTH, BOXART_HEIGHT, png_decoder_callback, b) != PNG_OK) {
free(b);
b = NULL;
}
}
}
return b;
}

View File

@ -122,8 +122,8 @@ void component_messagebox_draw (char *fmt, ...) {
}, FNT_DEFAULT, formatted, &paragraph_nbytes);
component_dialog_draw(
paragraph->bbox[2] - paragraph->bbox[0] + MESSAGEBOX_MARGIN,
paragraph->bbox[3] - paragraph->bbox[1] + MESSAGEBOX_MARGIN
paragraph->bbox.x1 - paragraph->bbox.x0 + MESSAGEBOX_MARGIN,
paragraph->bbox.y1 - paragraph->bbox.y0 + MESSAGEBOX_MARGIN
);
rdpq_paragraph_render(paragraph, DISPLAY_CENTER_X - (MESSAGEBOX_MAX_WIDTH / 2), VISIBLE_AREA_Y0);

View File

@ -69,15 +69,15 @@ void component_context_menu_draw (component_context_menu_t *cm) {
rdpq_paragraph_t *layout = rdpq_paragraph_builder_end();
int width = layout->bbox[2] - layout->bbox[0] + MESSAGEBOX_MARGIN;
int height = layout->bbox[3] - layout->bbox[1] + MESSAGEBOX_MARGIN;
int width = layout->bbox.x1 - layout->bbox.x0 + MESSAGEBOX_MARGIN;
int height = layout->bbox.y1 - layout->bbox.y0 + MESSAGEBOX_MARGIN;
component_dialog_draw(width, height);
int highlight_x0 = DISPLAY_CENTER_X - (width / 2);
int highlight_x1 = DISPLAY_CENTER_X + (width / 2);
int highlight_height = (layout->bbox[3] - layout->bbox[1]) / layout->nlines;
int highlight_y = VISIBLE_AREA_Y0 + layout->bbox[1] + ((cm->selected) * highlight_height);
int highlight_height = (layout->bbox.y1 - layout->bbox.y0) / layout->nlines;
int highlight_y = VISIBLE_AREA_Y0 + layout->bbox.y0 + ((cm->selected) * highlight_height);
component_box_draw(
highlight_x0,

View File

@ -105,7 +105,7 @@ void component_file_list_draw (entry_t *list, int entries, int selected) {
layout = rdpq_paragraph_builder_end();
int highlight_height = (layout->bbox[3] - layout->bbox[1]) / layout->nlines;
int highlight_height = (layout->bbox.y1 - layout->bbox.y0) / layout->nlines;
int highlight_y = VISIBLE_AREA_Y0 + TEXT_MARGIN_VERTICAL + ((selected - starting_position) * highlight_height);
component_box_draw(

212
src/menu/disk_info.c Normal file
View File

@ -0,0 +1,212 @@
#include <fatfs/ff.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include "disk_info.h"
#include "utils/fs.h"
#define SECTORS_PER_BLOCK (85)
#define DISK_ZONES (16)
#define DISK_BAD_TRACKS_PER_ZONE (12)
#define SYSTEM_AREA_SECTOR_LENGTH (232)
#define SYSTEM_AREA_LBA_LENGTH (SYSTEM_AREA_SECTOR_LENGTH * SECTORS_PER_BLOCK)
#define SYSTEM_AREA_LBA_COUNT (24)
#define SYSTEM_DATA_LBA_COUNT (8)
#define DISK_ID_LBA_COUNT (2)
#define DEV_SYSTEM_DATA_SECTOR_LENGTH (192)
#define RETAIL_SYSTEM_DATA_SECTOR_LENGTH (232)
#define REGION_ID_DEVELOPMENT (0x00000000)
#define REGION_ID_JAPANESE (0xE848D316)
#define REGION_ID_USA (0x2263EE56)
#define GET_U32(b) (((b)[0] << 24) | ((b)[1] << 16) | ((b)[2] << 8) | (b)[3])
static const int tracks_per_zone[DISK_ZONES] = {
158, 158, 149, 149, 149, 149, 149, 114, 158, 158, 149, 149, 149, 149, 149, 114
};
static const int system_data_lbas[SYSTEM_DATA_LBA_COUNT] = {
9, 8, 1, 0, 11, 10, 3, 2
};
static const int disk_id_lbas[DISK_ID_LBA_COUNT] = {
15, 14
};
static bool load_system_area_lba (FIL *fil, int lba, uint8_t *buffer) {
UINT bytes_read;
if (lba >= SYSTEM_AREA_LBA_COUNT) {
return true;
}
if (f_lseek(fil, SYSTEM_AREA_LBA_LENGTH * lba) != FR_OK) {
return true;
}
if (f_read(fil, buffer, SYSTEM_AREA_LBA_LENGTH, &bytes_read) != FR_OK) {
return true;
}
if (bytes_read != SYSTEM_AREA_LBA_LENGTH) {
return true;
}
return false;
}
static bool verify_system_area_lba (uint8_t *buffer, int sector_length) {
for (int sector = 1; sector < SECTORS_PER_BLOCK; sector++) {
for (int i = 0; i < sector_length; i++) {
if (buffer[i] != buffer[(sector * sector_length) + i]) {
return false;
}
}
}
return true;
}
static bool verify_system_data_lba (uint8_t *buffer) {
return (
(buffer[4] == 0x10) &&
((buffer[5] & 0xF0) == 0x10) &&
((buffer[5] & 0x0F) <= DISK_TYPE_6) &&
(GET_U32(&buffer[24]) == 0xFFFFFFFF)
);
}
static bool set_defect_tracks (uint8_t *buffer, disk_info_t *disk_info) {
for (int head_zone = 0; head_zone < DISK_ZONES; head_zone++) {
uint8_t start = ((head_zone == 0) ? 0 : buffer[7 + head_zone]);
uint8_t end = buffer[7 + head_zone + 1];
if ((end - start) > DISK_BAD_TRACKS_PER_ZONE) {
return false;
}
int track = 0;
for (int offset = start; offset < end; offset++) {
disk_info->defect_tracks[head_zone][track++] = buffer[0x20 + offset];
}
for (int i = 0; track < DISK_BAD_TRACKS_PER_ZONE; i++) {
disk_info->defect_tracks[head_zone][track++] = tracks_per_zone[head_zone] - i - 1;
}
}
return true;
}
static void update_bad_system_area_lbas (disk_info_t *disk_info) {
if (disk_info->region == DISK_REGION_DEVELOPMENT) {
disk_info->bad_system_area_lbas[0] = true;
disk_info->bad_system_area_lbas[1] = true;
disk_info->bad_system_area_lbas[8] = true;
disk_info->bad_system_area_lbas[9] = true;
} else {
disk_info->bad_system_area_lbas[2] = true;
disk_info->bad_system_area_lbas[3] = true;
disk_info->bad_system_area_lbas[10] = true;
disk_info->bad_system_area_lbas[11] = true;
disk_info->bad_system_area_lbas[12] = true;
}
for (int lba = 16; lba < SYSTEM_AREA_LBA_COUNT; lba++) {
disk_info->bad_system_area_lbas[lba] = true;
}
}
static disk_err_t load_and_verify_system_data_lba (FIL *fil, disk_info_t *disk_info) {
uint8_t buffer[SYSTEM_AREA_LBA_LENGTH];
int sector_length;
bool valid_system_data_lba_found = false;
for (int i = 0; i < SYSTEM_DATA_LBA_COUNT; i++) {
int lba = system_data_lbas[i];
if (load_system_area_lba(fil, lba, buffer)) {
return DISK_ERR_IO;
}
switch (GET_U32(&buffer[0])) {
case REGION_ID_DEVELOPMENT:
disk_info->region = DISK_REGION_DEVELOPMENT;
sector_length = DEV_SYSTEM_DATA_SECTOR_LENGTH;
break;
case REGION_ID_JAPANESE:
disk_info->region = DISK_REGION_JAPANESE;
sector_length = RETAIL_SYSTEM_DATA_SECTOR_LENGTH;
break;
case REGION_ID_USA:
disk_info->region = DISK_REGION_USA;
sector_length = RETAIL_SYSTEM_DATA_SECTOR_LENGTH;
break;
default:
disk_info->bad_system_area_lbas[lba] = true;
continue;
}
if (verify_system_area_lba(buffer, sector_length) && verify_system_data_lba(buffer) && set_defect_tracks(buffer, disk_info)) {
valid_system_data_lba_found = true;
disk_info->disk_type = (buffer[5] & 0x0F);
} else {
disk_info->bad_system_area_lbas[lba] = true;
}
}
return valid_system_data_lba_found ? DISK_OK : DISK_ERR_INVALID;
}
static disk_err_t load_and_verify_disk_id_lba (FIL *fil, disk_info_t *disk_info) {
uint8_t buffer[SYSTEM_AREA_LBA_LENGTH];
bool valid_disk_id_lba_found = false;
for (int i = 0; i < DISK_ID_LBA_COUNT; i++) {
int lba = disk_id_lbas[i];
if (load_system_area_lba(fil, lba, buffer)) {
return DISK_ERR_IO;
}
if (verify_system_area_lba(buffer, SYSTEM_AREA_SECTOR_LENGTH)) {
valid_disk_id_lba_found = true;
memcpy(disk_info->id, &buffer[0], sizeof(disk_info->id));
disk_info->version = buffer[4];
} else {
disk_info->bad_system_area_lbas[lba] = true;
}
}
return valid_disk_id_lba_found ? DISK_OK : DISK_ERR_INVALID;
}
disk_err_t disk_info_load (char *path, disk_info_t *disk_info) {
FIL fil;
disk_err_t err;
for (int i = 0; i < SYSTEM_AREA_LBA_COUNT; i++) {
disk_info->bad_system_area_lbas[i] = false;
}
if (f_open(&fil, strip_sd_prefix(path), FA_READ) != FR_OK) {
return DISK_ERR_NO_FILE;
}
if ((err = load_and_verify_system_data_lba(&fil, disk_info)) != DISK_OK) {
f_close(&fil);
return err;
}
if ((err = load_and_verify_disk_id_lba(&fil, disk_info)) != DISK_OK) {
f_close(&fil);
return err;
}
if (f_close(&fil) != FR_OK) {
return DISK_ERR_IO;
}
update_bad_system_area_lbas(disk_info);
return DISK_OK;
}

52
src/menu/disk_info.h Normal file
View File

@ -0,0 +1,52 @@
/**
* @file disk_info.h
* @brief 64DD disk information
* @ingroup menu
*/
#ifndef DISK_INFO_H__
#define DISK_INFO_H__
#include <stdbool.h>
#include <stdint.h>
typedef enum {
DISK_OK,
DISK_ERR_IO,
DISK_ERR_NO_FILE,
DISK_ERR_INVALID,
} disk_err_t;
typedef enum {
DISK_REGION_DEVELOPMENT,
DISK_REGION_JAPANESE,
DISK_REGION_USA,
} disk_region_t;
typedef enum {
DISK_TYPE_0,
DISK_TYPE_1,
DISK_TYPE_2,
DISK_TYPE_3,
DISK_TYPE_4,
DISK_TYPE_5,
DISK_TYPE_6,
} disk_type_t;
typedef struct {
disk_region_t region;
disk_type_t disk_type;
char id[4];
uint8_t version;
bool bad_system_area_lbas[24];
uint8_t defect_tracks[16][12];
} disk_info_t;
disk_err_t disk_info_load (char *path, disk_info_t *disk_info);
#endif

34
src/menu/hdmi.c Normal file
View File

@ -0,0 +1,34 @@
#include <libdragon.h>
#include "hdmi.h"
#define ROM_ADDRESS (0x10000000)
#define DDIPL_ADDRESS (0x06000000)
void hdmi_clear_game_id (void) {
pixelfx_clear_game_id();
}
void hdmi_send_game_id (boot_params_t *boot_params) {
uint8_t rom_header[0x40] __attribute__((aligned(8)));
uint32_t pi_address = ROM_ADDRESS;
if (boot_params->device_type == BOOT_DEVICE_TYPE_64DD) {
pi_address = DDIPL_ADDRESS;
}
dma_read_async(rom_header, pi_address, sizeof(rom_header));
dma_wait();
uint64_t rom_check_code;
uint8_t media_format;
uint8_t region_code;
memcpy((uint8_t *) (&rom_check_code), rom_header + 0x10, sizeof(rom_check_code));
media_format = rom_header[0x3B];
region_code = rom_header[0x3E];
pixelfx_send_game_id(rom_check_code, media_format, region_code);
}

12
src/menu/hdmi.h Normal file
View File

@ -0,0 +1,12 @@
#ifndef HDMI_H__
#define HDMI_H__
#include "boot/boot.h"
void hdmi_clear_game_id (void);
void hdmi_send_game_id (boot_params_t *boot_params);
#endif

View File

@ -8,6 +8,7 @@
#include "boot/boot.h"
#include "flashcart/flashcart.h"
#include "fonts.h"
#include "hdmi.h"
#include "menu_state.h"
#include "menu.h"
#include "mp3_player.h"
@ -53,12 +54,12 @@ static void frame_counter_reset (void) {
}
static void menu_init (boot_params_t *boot_params) {
controller_init();
joypad_init();
timer_init();
rtc_init();
rspq_init();
rdpq_init();
fonts_init();
sound_init_default();
menu = calloc(1, sizeof(menu_t));
@ -67,8 +68,8 @@ static void menu_init (boot_params_t *boot_params) {
menu->mode = MENU_MODE_NONE;
menu->next_mode = MENU_MODE_STARTUP;
menu->flashcart_error = flashcart_init();
if (menu->flashcart_error != FLASHCART_OK) {
menu->flashcart_err = flashcart_init();
if (menu->flashcart_err != FLASHCART_OK) {
menu->next_mode = MENU_MODE_FAULT;
}
@ -88,21 +89,31 @@ static void menu_init (boot_params_t *boot_params) {
menu->browser.valid = false;
menu->browser.directory = path_init("sd:/", init_directory);
menu->load.rom_path = NULL;
hdmi_clear_game_id();
tv_type = get_tv_type();
if ((tv_type == TV_PAL) && menu->settings.pal60) {
if ((tv_type == TV_PAL) && menu->settings.pal60_enabled) {
// HACK: Set TV type to NTSC, so PAL console would output 60 Hz signal instead.
TV_TYPE_RAM = TV_NTSC;
}
display_init(RESOLUTION_640x480, DEPTH_16_BPP, 2, GAMMA_NONE, ANTIALIAS_OFF);
display_init(RESOLUTION_640x480, DEPTH_16_BPP, 2, GAMMA_NONE, FILTERS_DISABLED);
register_VI_handler(frame_counter_handler);
fonts_init();
}
static void menu_deinit (menu_t *menu) {
unregister_VI_handler(frame_counter_handler);
// NOTE: Restore previous TV type so boot procedure wouldn't passthrough wrong value.
TV_TYPE_RAM = tv_type;
hdmi_send_game_id(menu->boot_params);
path_free(menu->browser.directory);
free(menu);
@ -110,13 +121,13 @@ static void menu_deinit (menu_t *menu) {
flashcart_deinit();
sound_close();
sound_deinit();
rdpq_close();
rspq_close();
rtc_close();
timer_close();
unregister_VI_handler(frame_counter_handler);
joypad_close();
display_close();
}
@ -136,6 +147,7 @@ static struct views_s {
{ view_music_player_init, view_music_player_display }, // MENU_MODE_MUSIC_PLAYER
{ view_credits_init, view_credits_display }, // MENU_MODE_CREDITS
{ view_load_rom_init, view_load_rom_display }, // MENU_MODE_LOAD_ROM
{ view_load_disk_init, view_load_disk_display }, // MENU_MODE_LOAD_DISK
{ view_load_emulator_init, view_load_emulator_display }, // MENU_MODE_LOAD_EMULATOR
{ view_error_init, view_error_display }, // MENU_MODE_ERROR
{ view_fault_init, view_fault_display }, // MENU_MODE_FAULT

View File

@ -9,9 +9,12 @@
#include <time.h>
#include "boot/boot.h"
#include "disk_info.h"
#include "flashcart/flashcart.h"
#include "path.h"
#include "rom_info.h"
#include "settings.h"
@ -29,6 +32,7 @@ typedef enum {
MENU_MODE_MUSIC_PLAYER,
MENU_MODE_CREDITS,
MENU_MODE_LOAD_ROM,
MENU_MODE_LOAD_DISK,
MENU_MODE_LOAD_EMULATOR,
MENU_MODE_ERROR,
MENU_MODE_FAULT,
@ -40,6 +44,7 @@ typedef enum {
typedef enum {
ENTRY_TYPE_DIR,
ENTRY_TYPE_ROM,
ENTRY_TYPE_DISK,
ENTRY_TYPE_EMULATOR,
ENTRY_TYPE_SAVE,
ENTRY_TYPE_IMAGE,
@ -61,9 +66,9 @@ typedef struct {
settings_t settings;
boot_params_t *boot_params;
flashcart_error_t flashcart_error;
char *error_message;
flashcart_err_t flashcart_err;
time_t current_time;
@ -72,9 +77,7 @@ typedef struct {
bool go_down;
bool go_left;
bool go_right;
bool fast;
int vertical_held_counter;
int horizontal_held_counter;
bool go_fast;
bool enter;
bool back;
@ -91,6 +94,13 @@ typedef struct {
entry_t *entry;
int selected;
} browser;
struct {
path_t *rom_path;
rom_info_t rom_info;
path_t *disk_path;
disk_info_t disk_info;
} load;
} menu_t;

View File

@ -1,278 +0,0 @@
#include <libdragon.h>
#include "rom_database.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
uint8_t extract_homebrew_setting(uint8_t setting, uint8_t bit_position) {
return (setting & (1 << bit_position)) ? 1 : 0;
}
/**
* @brief Normalize the Homebrew save type.
*/
uint8_t extract_homebrew_save_type(uint8_t save_type) {
switch (save_type) {
case HB_SAVE_TYPE_NONE:
return DB_SAVE_TYPE_NONE;
case HB_SAVE_TYPE_EEPROM_4K:
return DB_SAVE_TYPE_EEPROM_4K;
case HB_SAVE_TYPE_EEPROM_16K:
return DB_SAVE_TYPE_EEPROM_16K;
case HB_SAVE_TYPE_SRAM:
return DB_SAVE_TYPE_SRAM;
case HB_SAVE_TYPE_SRAM_BANKED:
return DB_SAVE_TYPE_SRAM_BANKED;
case HB_SAVE_TYPE_FLASHRAM:
return DB_SAVE_TYPE_FLASHRAM;
case HB_SAVE_TYPE_SRAM_128K:
return DB_SAVE_TYPE_SRAM_128K;
default:
return DB_SAVE_TYPE_INVALID; // Invalid save type, handle accordingly
}
}
/**
* @brief Reads the N64 ROM header from a file. @see https://n64brew.dev/wiki/ROM_Header
*/
rom_header_t file_read_rom_header(char *path) {
FILE *fp = fopen(path, "rb");
debugf("loading path: %s\n", path);
if (!fp) {
debugf("Error loading rom file header\n");
}
rom_header_t *rom_header = malloc(sizeof(rom_header_t));
fseek(fp, 0x00, SEEK_SET);
fread(&(rom_header->config_flags), sizeof(rom_header->config_flags), 1, fp);
// FIXME: handle endian appropriately, perhaps: cart_card_byteswap
fseek(fp, 0x04, SEEK_SET);
fread(&(rom_header->clock_rate), sizeof(rom_header->clock_rate), 1, fp);
fseek(fp, 0x08, SEEK_SET);
fread(&(rom_header->boot_address), sizeof(rom_header->boot_address), 1, fp);
fseek(fp, 0x0C, SEEK_SET);
fread(&(rom_header->sdk_version), sizeof(rom_header->sdk_version), 1, fp);
fseek(fp, 0x10, SEEK_SET);
fread(&(rom_header->checksum), sizeof(rom_header->checksum), 1, fp);
fseek(fp, 0x18, SEEK_SET);
fread(&(rom_header->unknown_reserved_1), sizeof(rom_header->unknown_reserved_1), 1, fp);
fseek(fp, 0x20, SEEK_SET);
fgets(rom_header->title, sizeof(rom_header->title), fp);
fseek(fp, 0x34, SEEK_SET);
fread(&(rom_header->unknown_reserved_2), sizeof(rom_header->unknown_reserved_2), 1, fp);
fseek(fp, 0x3B, SEEK_SET);
fread(&(rom_header->metadata.media_type), sizeof(rom_header->metadata.media_type), 1, fp);
fseek(fp, 0x3C, SEEK_SET);
fread(&(rom_header->metadata.unique_identifier), sizeof(rom_header->metadata.unique_identifier), 1, fp);
fseek(fp, 0x3E, SEEK_SET);
fread(&(rom_header->metadata.destination_market), sizeof(rom_header->metadata.destination_market), 1, fp);
fseek(fp, 0x3F, SEEK_SET);
fread(&(rom_header->metadata.version), sizeof(rom_header->metadata.version), 1, fp);
fseek(fp, 0x40, SEEK_SET);
fread(&(rom_header->ipl_boot_code), sizeof(rom_header->ipl_boot_code), 1, fp);
fclose(fp);
// FIXME: memory leak, rom header is copied, pointer should be returned instead or data freed before returning
return *rom_header;
}
// TODO: make sure this can also handle an external input file.
uint8_t rom_db_match_save_type(rom_header_t rom_header) {
// These are ordered to ensure they are handled correctly... (presumes big endian)
// First: Match by the `ED` or `HB` Developer ID
if (rom_header.metadata.unique_identifier == *(uint16_t *)"ED" || rom_header.metadata.unique_identifier == *(uint16_t *)"HB") {
// #ifdef ED64_COMPATIBLE
// uint8_t low_nibble = rom_header.metadata.version & 0x0F;
// uint8_t rtc_enabled = extract_homebrew_setting(low_nibble, 0); // Bit 0
// uint8_t region_free_enabled = extract_homebrew_setting(low_nibble, 1); // Bit 1
// #endif
uint8_t high_nibble = (rom_header.metadata.version >> 4) & 0x0F;
return extract_homebrew_save_type(high_nibble);
}
// Second: Match the default entries for crc.
// DOUBUTSU BANCHOU (ANIMAL LEADER, Cubivore) - Contains no game ID
if (rom_header.checksum == 0xEB85EBC9596682AF) return DB_SAVE_TYPE_FLASHRAM;
// DK Retail kiosk demo (shares ID with Dinosaur planet, but hacks are unlikely)!
if (rom_header.checksum == 0x0DD4ABABB5A2A91E) return DB_SAVE_TYPE_EEPROM_16K;
// DMTJ 64DD game
// if (rom_header.checksum == 0x4cbc3b56) return DB_SAVE_TYPE_SRAM;
// FIXME: we need to take into account the Category (first char) and the Region code (last char) before a general match of the ID.
// Finally: Match the default entries for "unique" ROM ID.
// It is useful to use this rather than CRC's so that ROM hacks continue to work.
static char *cart_ids[] = {
// EEP4K
"AB", "AD", "AG", "BC", "BD", "BH", "BK", "BM", "BN", "BV", "B6", "CG", "CH", "CR", "CT", "CU",
"CX", "DR", "DQ", "DU", "DY", "D3", "D4", "EA", "ER", "FG", "FH", "FW", "FX", "FY", "GC", "GE",
"GF", "GU", "GV", "HA", "HF", "HP", "IC", "IJ", "IR", "JM", "K2", "KA", "KI", "KT", "LB", "LR",
"MG", "MI", "ML", "MO", "MR", "MS", "MU", "MW", "N6", "NA", "OH", "PG", "PW", "PY", "RC", "RS",
"S6", "SA", "SC", "SM", "SU", "SV", "SW", "TB", "TC", "TJ", "TM", "TN", "TP", "TR", "TX", "T6",
"VL", "VY", "WC", "WL", "WQ", "WR", "WU", "XO", "4W", "GL", "O2", "OS", "PM", "PT", "SN", "SB",
"SS",
// EEP16K
"B7", "CW", "CZ", "DO", "D2", "D6", "EP", "EV", "FU", "F2", "IM", "M8", "MV", "MX", "NB", "NX",
"PD", "RZ", "UB", "X7", "YS", "3D", "R7",
// SRAM
"AL", "AY", "A2", "DA", "FZ", "GP", "G6", "K4", "KG", "MF", "OB", "RE", "RI", "TE", "VB", "VP",
"WI", "W2", "WX", "WZ", "YW", "ZL", "B5", "IB", "JG", "UT", "UM", "T3",
// SRAM_BANKED
"DZ",
// SRAM_128K
// FLASHRAM
"AF", "CC", "CK", "DL", "DP", "JD", "JF", "KJ", "M6", "MQ", "P2", "P3", "PF", "PH", "PN", "PO",
"PS", "RH", "SI", "SQ", "T9", "W4", "ZS", "DA",
// Controller Pak required
"2V", "32", "3P", "3T", "8W", "AC", "AM", "AR", "B4", "BF", "BL", "BO", "BP", "BQ", "BR", "BX",
"BY", "CD", "CE", "CL", "CO", "CS", "DE", "DH", "DM", "DN", "DT", "EG", "ET", "F9", "FD", "FF",
"FO", "FQ", "G2", "G5", "GA", "GM", "GN", "GR", "GX", "HC", "HT", "HX", "HW", "HV", "IV", "KK",
"L2", "LG", "M4", "MB", "MY", "NC", "NS", "O7", "OF", "2P", "PR", "PU", "PX", "R2", "R3", "R6",
"RK", "RO", "RP", "RR", "RT", "RU", "RV", "RX", "SD", "SF", "SG", "SK", "SL", "SP", "SX", "SY",
"T4", "TA", "TF", "TH", "TQ", "WB", "X2", "X3", "Y2", "V3", "V8", "VC", "VG", "VR", "WD", "WO",
"WP", "WT", "XF", "YP", "ZO",
// Definitely none
// "JQ", // Batman Beyond: Return of the Joker
// "CB", // Charlie Blast's Territory (Password)
// "CF", // Clayfighter 63 1/3
// "C2", // Clayfighter: Sculptor's Cut
// "DK", // Dark Rift
// "EL", // Elmo's Letter Adventure
// Elmo's Number Journey
// "JO", // Jeopardy!
// "KE", // Knife Edge: Nosegunner
// "ME", // Mace: The Dark Age
// "MT", //Magical Tetris Challenge
// "M3", //Monster Truck Madness (Password)
// "MK", //Mortal Kombat Trilogy
// Powerpuff Girls: Chemical X Traction (Password)
// "RG", //Rugrats: Scavenger Hunt
// "CY", //South Park: Chef's Luv Shack
// "OH", //Transformers Beast Wars: Transmetals
// "WA", //War Gods
// "WF", //Wheel of Fortune
// To work out
// "AH", "BW", "GB", "LC", "2V", "DW", "M3", "MK", "NC", "WA",
// Last entry
"\0\0"
};
static int save_types[] = {
// EEP4K
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01,
// EEP16K
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
// SRAM
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
// SRAM_BANKED
0x04,
// SRAM_128K
// FLASHRAM
0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
// Controller Pak required
0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
0x10, 0x10, 0x10, 0x10, 0x10,
// Last entry.
0xFF
};
for (int i = 0; save_types[i] != 0xFF; i++) {
if (rom_header.metadata.unique_identifier == *(uint16_t *) cart_ids[i]) {
return save_types[i];
}
}
return DB_SAVE_TYPE_NONE; //Nothing matched.
}
uint8_t rom_db_match_expansion_pak(rom_header_t rom_header) {
// Space Station Silicon Valley has known issues on NTSC.
// We only check for the known working market here.
if ( (rom_header.metadata.unique_identifier == *(uint16_t *) "SV") && (rom_header.metadata.destination_market != MARKET_EUROPEAN_BASIC)) {
return DB_MEMORY_EXPANSION_FAULTY;
}
static char *cart_ids[] = {
// See: https://nintendo.fandom.com/wiki/Nintendo_64_Expansion_Pak
// Expansion Pak Required
"DO", "DP", "ZS", // Donkey Kong, Dino Planet, Majoras Mask
// Expansion Pak Recommended (games are pretty broken without it)
"IJ", "PD", "SQ", // Indiana Jones, Perfect Dark, Starcraft
// Expansion Pak Enhanced (box art suggest improvements in some form)
"3T", "2M", "32", "4W", "9F", "AC", "AM", "AR", "AS", "AY", "BE", "CC", "CE", "CO", "D4", "DT",
"DQ", "DZ", "F2", "FL", "GB", "GX", "HT", "HV", "IC", "IS", "JA", "L2", "MD", "MX", "NA", "O7",
"Q2", "Q9", "QB", "RC", "RE", "RO", "RU", "RS", "RV", "RW", "SD", "SL", "P3", "T2", "T4", "TF",
"TK", "Y2", "TQ", "V8", "VG", "XF", "ZO",
// Last entry
"\0\0"
};
static int exp_types[] = {
// Expansion Pak Required
0x01, 0x01, 0x01,
// Expansion Pak Recommended
0x02, 0x02, 0x02,
// Expansion Pak Enhanced
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
// Last entry.
0xFF
};
for (int i = 0; exp_types[i] != 0xff; i++) {
if (rom_header.metadata.unique_identifier == *(uint16_t *) cart_ids[i]) {
return exp_types[i];
}
}
return DB_MEMORY_EXPANSION_NONE;
}

View File

@ -1,244 +0,0 @@
/**
* @file rom_database.h
* @brief N64 ROM Database.
* @note Only works with N64 ROM's by checking the first 1024 bytes of the file.
* @ingroup menu
*/
#ifndef ROM_DATABASE_H__
#define ROM_DATABASE_H__
#include <stdint.h>
/**
* @brief ROM database save type enumeration.
* @note These values are independent of flashcart / OS
* but by default align to SC64.
*/
typedef enum {
/** @brief The ROM has no save type. */
DB_SAVE_TYPE_NONE = 0x00,
/** @brief The ROM uses EEPROM 4K saves. */
DB_SAVE_TYPE_EEPROM_4K = 0x01,
/** @brief The ROM uses EEPROM 16K saves. */
DB_SAVE_TYPE_EEPROM_16K = 0x02,
/** @brief The ROM uses SRAM saves. */
DB_SAVE_TYPE_SRAM = 0x03,
/** @brief The ROM uses SRAM Banked saves. */
DB_SAVE_TYPE_SRAM_BANKED = 0x04,
/** @brief The ROM uses SRAM 128K saves @note This is not supported by all flashcarts. */
DB_SAVE_TYPE_SRAM_128K = 0x05,
/** @brief The ROM uses FLASHRAM saves. */
DB_SAVE_TYPE_FLASHRAM = 0x06,
/** @brief The ROM uses CPAK saves @note This must be handled by user code. */
DB_SAVE_TYPE_CPAK = 0x10,
/** @brief The ROM uses Disk Drive saves @note This is not supported by all flashcarts. */
DB_SAVE_TYPE_DD = 0x20,
/** @brief The ROM uses Disk Drive conversion saves @note This must be handled by user code. */
DB_SAVE_TYPE_DD_CONVERSION = 0x30,
/** @brief The ROM uses a save type that was not recognised. */
DB_SAVE_TYPE_INVALID = 0xFF,
} db_savetype_t;
/** @brief ROM system memory requirements enumeration. */
typedef enum {
/** @brief The ROM is happy with 4MB of memory. */
DB_MEMORY_EXPANSION_NONE = 0x00,
/** @brief The ROM requires 8MB of memory. */
DB_MEMORY_EXPANSION_REQUIRED = 0x01,
/** @brief The ROM recommends 8MB of memory. */
DB_MEMORY_EXPANSION_RECOMMENDED = 0x02,
/** @brief The ROM suggests 8MB of memory. */
DB_MEMORY_EXPANSION_SUGGESTED = 0x03,
/** @brief The ROM is faulty when using 8MB of memory. */
DB_MEMORY_EXPANSION_FAULTY = 0x04,
} rom_memorytype_t;
/**
* @brief ROM homebrew save type enumeration.
* @note These align to the Krikzz ED64 save types and are generally accepted
* by all emulators.
*
*/
typedef enum {
/** @brief The ROM has no save type. */
HB_SAVE_TYPE_NONE = 0x00,
/** @brief The ROM uses EEPROM 4K saves. */
HB_SAVE_TYPE_EEPROM_4K = 0x01,
/** @brief The ROM uses EEPROM 16K saves. */
HB_SAVE_TYPE_EEPROM_16K = 0x02,
/** @brief The ROM uses SRAM saves. */
HB_SAVE_TYPE_SRAM = 0x03,
/** @brief The ROM uses SRAM Banked saves. */
HB_SAVE_TYPE_SRAM_BANKED = 0x04,
/** @brief The ROM uses FLASHRAM saves. */
HB_SAVE_TYPE_FLASHRAM = 0x05,
/** @brief The ROM uses SRAM 128K saves @note This is not supported by all flashcarts. */
HB_SAVE_TYPE_SRAM_128K = 0x06,
} homebrew_savetype_t;
/**
* @brief ROM file endian enumeration.
*
* @note this is a hack used for checking ROM's against expected Big Endian
* when reading from the file system.
*/
typedef enum {
/** @brief Big Endian ROM */
ROM_BIG_ENDIAN = 0x80371240,
/** @brief Little Endian ROM */
ROM_LITTLE_ENDIAN = 0x40123780,
/** @brief Mid Big Endian ROM */
ROM_MID_BIG_ENDIAN = 0x37804012,
/** @brief Mid Little Endian ROM */
ROM_MID_LITTLE_ENDIAN = 0x12408037,
/** @brief Big Endian IPL ROM */
IPL_BIG_ENDIAN = 0x80270740,
} rom_endian_type_t;
/** @brief ROM media type enumeration. */
typedef enum {
/** @brief Is a stand alone Cartridge program. */
N64_CART = 'N',
/** @brief Is a stand alone Disk Drive program. */
N64_DISK = 'D',
/** @brief Is a Cartridge program that could use an extra Disk Drive program to expand its capabilities. */
N64_CART_EXPANDABLE = 'C',
/** @brief Is a Disk Drive program that could use an extra Cartridge program to expand its capabilities. */
N64_DISK_EXPANDABLE = 'E',
/** @brief Is an Aleck64 program. */
N64_ALECK64 = 'Z'
} rom_media_type_t;
/** @brief ROM market region & language type enumeration. */
typedef enum {
/** @brief The ROM is designed for Japanese and "English" languages. */
MARKET_JAPANESE_MULTI = 'A', // 1080 Snowboarding JPN is the only ROM that uses this? possibily a mistake, or the fact it also includes American English!.
/** @brief The ROM is designed for Brazil (Portuguese) language. */
MARKET_BRAZILIAN = 'B',
/** @brief The ROM is designed for Chinese language. */
MARKET_CHINESE = 'C',
/** @brief The ROM is designed for German language. */
MARKET_GERMAN = 'D',
/** @brief The ROM is designed for North American "English" language. */
MARKET_NORTH_AMERICA = 'E',
/** @brief The ROM is designed for French language. */
MARKET_FRENCH = 'F',
/** @brief The ROM is designed for a NTSC Gateway 64. */
MARKET_GATEWAY64_NTSC = 'G',
/** @brief The ROM is designed for Dutch language. */
MARKET_DUTCH = 'H',
/** @brief The ROM is designed for Italian language. */
MARKET_ITALIAN = 'I',
/** @brief The ROM is designed for Japanese language. */
MARKET_JAPANESE = 'J',
/** @brief The ROM is designed for Korean language. */
MARKET_KOREAN = 'K',
/** @brief The ROM is designed for a PAL Gateway 64. */
MARKET_GATEWAY64_PAL = 'L',
/** @brief The ROM is designed for Canada region (English and French) language. */
MARKET_CANADIAN = 'N',
/** @brief The ROM is designed for European market and languages (must at minimum include English). */
MARKET_EUROPEAN_BASIC = 'P', // Sometimes used for Australian region ROMs as well.
/** @brief The ROM is designed for Spanish language */
MARKET_SPANISH = 'S',
/** @brief The ROM is designed for Australia (English) language. */
MARKET_AUSTRALIAN = 'U',
/** @brief The ROM is designed for Scandinavian (Swedish, Norwegian, Finnish, etc.) languages. */
MARKET_SCANDINAVIAN = 'W',
/** @brief The ROM is designed for an undefined region and TBD language(s). */
MARKET_OTHER_X = 'X', // many EU ROM's, Top Gear Rally (Asia) and HSV Racing (AUS) ROM uses this.
/** @brief The ROM is designed for a European region and language(s). */
MARKET_OTHER_Y = 'Y', // many EU ROM's uses this.
/** @brief The ROM is designed for an undefined region and TBD language(s). */
MARKET_OTHER_Z = 'Z' // no known ROM's use this.
} rom_destination_market_t;
/**
* @brief ROM Config Flags Structure
* @note This information is derived from the ROM header.
* @see https://n64brew.dev/wiki/Peripheral_Interface#Domains
* i.e.
* 0x00 = PI BSD Domain 1 Release register
* 0x01 = PI BSD Domain 1 Page Size register
* 0x02 = PI BSD Domain 1 Pulse Width register
* 0x03 = PI BSD Domain 1 Latch register
*/
typedef struct {
/* PI BSD Domain 1 Release register value */
uint8_t domain1_release;
/* PI BSD Domain 1 Page Size register value */
uint8_t domain1_page_size;
/* PI BSD Domain 1 Pulse Width register value */
uint8_t domain1_latency;
/* PI BSD Domain 1 Latch register value */
uint8_t domain1_pulse_width;
} rom_config_flags_t;
/**
* @brief ROM Metadata Structure
* @note This information is derived from the ROM header. i.e.
* 0x3B = Media Type
* 0x3C and 0x3D = Unique Identifier
* 0x3E = Destination Market
*/
typedef struct {
uint8_t media_type; // rom_media_type_t
uint16_t unique_identifier;
uint8_t destination_market; // rom_destination_market_t
uint8_t version;
} rom_metadata_t;
/**
* @brief ROM Header Structure
* @note This information is derived from the ROM header. @see https://n64brew.dev/wiki/ROM_Header
*/
typedef struct {
/** @brief The ROM configuration flags @note we currently use this to work out the endian @see rom_endian_type_t. */
uint32_t config_flags; // TODO: use rom_config_flags_t
/** @brief The ROM file clock rate. */
uint32_t clock_rate;
/** @brief The ROM file boot address. */
uint32_t boot_address;
/** @brief The ROM file SDK version. */
uint32_t sdk_version;
/** @brief The ROM file checksum. */
uint64_t checksum;
/** @brief The ROM file unknown reserved region at 0x18. for 8 bytes */
uint64_t unknown_reserved_1;
/** @brief The ROM file title */
char title[21]; // 20 chars + null char
/** @brief The ROM file unknown reserved region at 0x34. for 7 bytes */
char unknown_reserved_2[7];
/** @brief The ROM file metadata @see rom_metadata_t. */
rom_metadata_t metadata;
/** @brief The ROM file release version. */
char ipl_boot_code[0x0FC0];
} rom_header_t;
#ifdef __cplusplus
extern "C" {
#endif
rom_header_t file_read_rom_header(char *path);
uint8_t rom_db_match_save_type(rom_header_t rom_header);
uint8_t rom_db_match_expansion_pak(rom_header_t rom_header);
#ifdef __cplusplus
}
#endif
#endif

738
src/menu/rom_info.c Normal file
View File

@ -0,0 +1,738 @@
#include <string.h>
#include <fatfs/ff.h>
#include "rom_info.h"
#include "utils/fs.h"
#define SWAP_VARS(x0, x1) { typeof(x0) tmp = (x0); (x0) = (x1); (x1) = (tmp); }
#define PI_CONFIG_BIG_ENDIAN (0x80371240)
#define PI_CONFIG_LITTLE_ENDIAN (0x40123780)
#define PI_CONFIG_BYTE_SWAPPED (0x37804012)
#define PI_CONFIG_64DD_IPL (0x80270740)
#define CLOCK_RATE_DEFAULT (0x0000000F)
typedef struct __attribute__((packed)) {
uint32_t pi_dom1_config;
uint32_t clock_rate;
uint32_t boot_address;
struct {
uint8_t __unused_1[2];
uint8_t version;
char revision;
} libultra;
uint64_t check_code;
uint8_t __unused_1[8];
char title[20];
uint8_t __unused_2[7];
union {
char game_code[4];
struct {
char category_code;
char unique_code[2];
char destination_code;
};
};
uint8_t version;
uint8_t ipl3[IPL3_LENGTH];
} rom_header_t;
typedef enum {
// Check only game code
MATCH_TYPE_ID,
// Check game code and region
MATCH_TYPE_ID_REGION,
// Check game code, region and version
MATCH_TYPE_ID_REGION_VERSION,
// Check game check code
MATCH_TYPE_CHECK_CODE,
// Check for homebrew header ID
MATCH_TYPE_HOMEBREW_HEADER,
// List end marker
MATCH_TYPE_END
} match_type_t;
typedef enum {
// No features supported
FEAT_NONE = 0,
// Controller Pak
FEAT_CPAK = (1 << 0),
// Rumble Pak
FEAT_RPAK = (1 << 1),
// Transfer Pak
FEAT_TPAK = (1 << 2),
// Voice Recognition Unit
FEAT_VRU = (1 << 3),
// Real Time Clock
FEAT_RTC = (1 << 4),
// Expansion Pak (for games that will not work without it inserted into the console)
FEAT_EXP_PAK_REQUIRED = (1 << 5),
// Expansion Pak (for games with game play enhancements)
FEAT_EXP_PAK_RECOMMENDED = (1 << 6),
// Expansion Pak (for games with visual (or other) enhancements)
FEAT_EXP_PAK_ENHANCED = (1 << 7),
// No Expansion Pak (for games "broken" with it inserted into the console)
FEAT_EXP_PAK_BROKEN = (1 << 8),
// 64DD disk to ROM conversion
FEAT_64DD_CONVERSION = (1 << 9),
// Combo ROM + Disk games
FEAT_64DD_ENHANCED = (1 << 10),
} feat_t;
typedef struct {
// Which fields to check
match_type_t type;
// Fields to check for matching
union {
struct {
// Game code (with media type and optional region) or unique ID
const char *id;
// Game version
uint8_t version;
};
// Game check code
uint64_t check_code;
} fields;
// Matched game metadata
struct {
// Save type (only cartridge save types)
save_type_t save;
// Supported features
feat_t feat;
} data;
} match_t;
#define MATCH_ID(i, s, f) { .type = MATCH_TYPE_ID, .fields = { .id = i }, .data = { .save = s, .feat = f } }
#define MATCH_ID_REGION(i, s, f) { .type = MATCH_TYPE_ID_REGION, .fields = { .id = i }, .data = { .save = s, .feat = f } }
#define MATCH_ID_REGION_VERSION(i, v, s, f) { .type = MATCH_TYPE_ID_REGION_VERSION, .fields = { .id = i, .version = v }, .data = { .save = s, .feat = f } }
#define MATCH_CHECK_CODE(c, s, f) { .type = MATCH_TYPE_CHECK_CODE, .fields = { .check_code = c }, .data = { .save = s, .feat = f } }
#define MATCH_HOMEBREW_HEADER(i) { .type = MATCH_TYPE_HOMEBREW_HEADER, .fields = { .id = i }, .data = { .feat = FEAT_NONE } }
#define MATCH_END { .type = MATCH_TYPE_END, .data = { .save = SAVE_TYPE_NONE, .feat = FEAT_NONE } }
// List shamelessly stolen from https://github.com/ares-emulator/ares/blob/master/mia/medium/nintendo-64.cpp
static const match_t database[] = {
MATCH_HOMEBREW_HEADER("ED"), // Homebrew header (ED)
MATCH_CHECK_CODE(0x000000004CBC3B56, SAVE_TYPE_SRAM, FEAT_EXP_PAK_REQUIRED | FEAT_64DD_CONVERSION), // DMTJ 64DD cartridge conversion
MATCH_CHECK_CODE(0x0DD4ABABB5A2A91E, SAVE_TYPE_EEPROM_16K, FEAT_EXP_PAK_REQUIRED), // DK Retail kiosk demo
MATCH_CHECK_CODE(0xEB85EBC9596682AF, SAVE_TYPE_FLASHRAM, FEAT_NONE), // Doubutsu Banchou
MATCH_ID_REGION_VERSION("NK4J", 0, SAVE_TYPE_SRAM, FEAT_RPAK), // Kirby 64: The Crystal Shards [Hoshi no Kirby 64 (J)]
MATCH_ID_REGION_VERSION("NK4J", 1, SAVE_TYPE_SRAM, FEAT_RPAK), // Kirby 64: The Crystal Shards [Hoshi no Kirby 64 (J)]
MATCH_ID("NK4", SAVE_TYPE_EEPROM_16K, FEAT_RPAK), // Kirby 64: The Crystal Shards [Hoshi no Kirby 64 (J)]
MATCH_ID_REGION_VERSION("NSMJ", 3, SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Super Mario 64 Shindou Edition
MATCH_ID("NSM", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Super Mario 64
MATCH_ID_REGION_VERSION("NWRJ", 2, SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Wave Race 64 Shindou Edition
MATCH_ID("NWR", SAVE_TYPE_EEPROM_4K, FEAT_CPAK), // Wave Race 64
MATCH_ID_REGION("N3HJ", SAVE_TYPE_SRAM, FEAT_NONE), // Ganbare! Nippon! Olympics 2000
MATCH_ID("N3H", SAVE_TYPE_NONE, FEAT_CPAK), // International Track & Field 2000
MATCH_ID_REGION("ND3J", SAVE_TYPE_EEPROM_16K, FEAT_RPAK), // Akumajou Dracula Mokushiroku (J)
MATCH_ID("ND3", SAVE_TYPE_NONE, FEAT_CPAK), // Castlevania
MATCH_ID_REGION("ND4J", SAVE_TYPE_EEPROM_16K, FEAT_RPAK), // Akumajou Dracula Mokushiroku Gaiden: Legend of Cornell (J)
MATCH_ID("ND4", SAVE_TYPE_NONE, FEAT_CPAK), // Castlevania - Legacy of Darkness
MATCH_ID_REGION("NDKJ", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Dark Rift [Space Dynamites (J)]
MATCH_ID_REGION("NSVE", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Space Station Silicon Valley
MATCH_ID("NSV", SAVE_TYPE_EEPROM_4K, FEAT_RPAK | FEAT_EXP_PAK_BROKEN), // Space Station Silicon Valley
MATCH_ID_REGION("NWTJ", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Wetrix
MATCH_ID("NWT", SAVE_TYPE_NONE, FEAT_CPAK), // Wetrix
MATCH_ID("CLB", SAVE_TYPE_EEPROM_4K, FEAT_RPAK | FEAT_64DD_ENHANCED), // Mario Party (NTSC)
MATCH_ID("NAB", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Air Boarder 64
MATCH_ID("NAD", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Worms Armageddon (U)
MATCH_ID("NAG", SAVE_TYPE_EEPROM_4K, FEAT_CPAK), // AeroGauge
MATCH_ID("NB6", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_TPAK), // Super B-Daman: Battle Phoenix 64
MATCH_ID("NBC", SAVE_TYPE_EEPROM_4K, FEAT_CPAK), // Blast Corps
MATCH_ID("NBD", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Bomberman Hero [Mirian Ojo o Sukue! (J)]
MATCH_ID("NBH", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Body Harvest
MATCH_ID("NBK", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Banjo-Kazooie [Banjo to Kazooie no Daiboken (J)]
MATCH_ID("NBM", SAVE_TYPE_EEPROM_4K, FEAT_CPAK), // Bomberman 64 [Baku Bomberman (J)]
MATCH_ID("NBN", SAVE_TYPE_EEPROM_4K, FEAT_CPAK), // Bakuretsu Muteki Bangaioh
MATCH_ID("NBV", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Bomberman 64: The Second Attack! [Baku Bomberman 2 (J)]
MATCH_ID("NCG", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK | FEAT_TPAK), // Choro Q 64 II - Hacha Mecha Grand Prix Race (J)
MATCH_ID("NCH", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Chopper Attack
MATCH_ID("NCR", SAVE_TYPE_EEPROM_4K, FEAT_CPAK), // Penny Racers [Choro Q 64 (J)]
MATCH_ID("NCT", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Chameleon Twist
MATCH_ID("NCU", SAVE_TYPE_EEPROM_4K, FEAT_CPAK), // Cruis'n USA
MATCH_ID("NCX", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Custom Robo
MATCH_ID("NDQ", SAVE_TYPE_EEPROM_4K, FEAT_CPAK), // Disney's Donald Duck - Goin' Quackers [Quack Attack (E)]
MATCH_ID("NDR", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Doraemon: Nobita to 3tsu no Seireiseki
MATCH_ID("NDU", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Duck Dodgers starring Daffy Duck
MATCH_ID("NDY", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Diddy Kong Racing
MATCH_ID("NEA", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // PGA European Tour
MATCH_ID("NER", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Aero Fighters Assault [Sonic Wings Assault (J)]
MATCH_ID("NF2", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // F-1 World Grand Prix II
MATCH_ID("NFG", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Fighter Destiny 2
MATCH_ID("NFH", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // In-Fisherman Bass Hunter 64
MATCH_ID("NFW", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // F-1 World Grand Prix
MATCH_ID("NFX", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Star Fox 64 [Lylat Wars (E)]
MATCH_ID("NFY", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Kakutou Denshou: F-Cup Maniax
MATCH_ID("NGE", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // GoldenEye 007
MATCH_ID("NGL", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Getter Love!!
MATCH_ID("NGU", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Tsumi to Batsu: Hoshi no Keishousha (Sin and Punishment)
MATCH_ID("NGV", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Glover
MATCH_ID("NHA", SAVE_TYPE_EEPROM_4K, FEAT_CPAK), // Bomberman 64: Arcade Edition (J)
MATCH_ID("NHF", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // 64 Hanafuda: Tenshi no Yakusoku
MATCH_ID("NHP", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Heiwa Pachinko World 64
MATCH_ID("NIC", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Indy Racing 2000
MATCH_ID("NIJ", SAVE_TYPE_EEPROM_4K, FEAT_RPAK | FEAT_EXP_PAK_RECOMMENDED), // Indiana Jones and the Infernal Machine
MATCH_ID("NIR", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Utchan Nanchan no Hono no Challenger: Denryuu Ira Ira Bou
MATCH_ID("NJM", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Earthworm Jim 3D
MATCH_ID("NK2", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Snowboard Kids 2 [Chou Snobow Kids (J)]
MATCH_ID("NKA", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Fighters Destiny [Fighting Cup (J)]
MATCH_ID("NKI", SAVE_TYPE_EEPROM_4K, FEAT_CPAK), // Killer Instinct Gold
MATCH_ID("NKT", SAVE_TYPE_EEPROM_4K, FEAT_CPAK), // Mario Kart 64
MATCH_ID("NLB", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Mario Party (PAL)
MATCH_ID("NLL", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Last Legion UX
MATCH_ID("NLR", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Lode Runner 3-D
MATCH_ID("NMG", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Monaco Grand Prix [Racing Simulation 2 (G)]
MATCH_ID("NMI", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Mission: Impossible
MATCH_ID("NML", SAVE_TYPE_EEPROM_4K, FEAT_RPAK | FEAT_TPAK), // Mickey's Speedway USA [Mickey no Racing Challenge USA (J)]
MATCH_ID("NMO", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Monopoly
MATCH_ID("NMR", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Multi-Racing Championship
MATCH_ID("NMS", SAVE_TYPE_EEPROM_4K, FEAT_CPAK), // Morita Shougi 64
MATCH_ID("NMU", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Big Mountain 2000
MATCH_ID("NMW", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Mario Party 2
MATCH_ID("NMZ", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Zool - Majou Tsukai Densetsu (J)
MATCH_ID("NN6", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Dr. Mario 64
MATCH_ID("NNA", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Star Wars Episode I: Battle for Naboo
MATCH_ID("NOS", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // 64 Oozumou
MATCH_ID("NP2", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Chou Kuukan Night Pro Yakyuu King 2 (J)
MATCH_ID("NPG", SAVE_TYPE_EEPROM_4K, FEAT_RPAK | FEAT_VRU), // Hey You, Pikachu! [Pikachu Genki Dechu (J)]
MATCH_ID("NPT", SAVE_TYPE_EEPROM_4K, FEAT_RPAK | FEAT_TPAK), // Puyo Puyon Party
MATCH_ID("NPW", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Pilotwings 64
MATCH_ID("NPY", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Puyo Puyo Sun 64
MATCH_ID("NRA", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Rally '99 (J)
MATCH_ID("NRC", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Top Gear Overdrive
MATCH_ID("NRS", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Star Wars: Rogue Squadron [Shutsugeki! Rogue Chuutai (J)]
MATCH_ID("NS3", SAVE_TYPE_EEPROM_4K, FEAT_CPAK), // AI Shougi 3
MATCH_ID("NS6", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Star Soldier: Vanishing Earth
MATCH_ID("NSA", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Sonic Wings Assault (J)
MATCH_ID("NSC", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Starshot: Space Circus Fever
MATCH_ID("NSN", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Snow Speeder (J)
MATCH_ID("NSS", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Super Robot Spirits
MATCH_ID("NSU", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Rocket: Robot on Wheels
MATCH_ID("NSW", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Star Wars: Shadows of the Empire [Teikoku no Kage (J)]
MATCH_ID("NT6", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Tetris 64
MATCH_ID("NTB", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Transformers: Beast Wars Metals 64 (J)
MATCH_ID("NTC", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // 64 Trump Collection
MATCH_ID("NTJ", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Tom & Jerry in Fists of Fury
MATCH_ID("NTM", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Mischief Makers [Yuke Yuke!! Trouble Makers (J)]
MATCH_ID("NTN", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // All Star Tennis '99
MATCH_ID("NTP", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Tetrisphere
MATCH_ID("NTR", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Top Gear Rally (J + E)
MATCH_ID("NTW", SAVE_TYPE_EEPROM_4K, FEAT_CPAK), // 64 de Hakken!! Tamagotchi
MATCH_ID("NTX", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Taz Express
MATCH_ID("NVL", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // V-Rally Edition '99
MATCH_ID("NVY", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // V-Rally Edition '99 (J)
MATCH_ID("NWC", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Wild Choppers
MATCH_ID("NWQ", SAVE_TYPE_EEPROM_4K, FEAT_CPAK | FEAT_RPAK), // Rally Challenge 2000
MATCH_ID("NWU", SAVE_TYPE_EEPROM_4K, FEAT_NONE), // Worms Armageddon (E)
MATCH_ID("NXO", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Cruis'n Exotica
MATCH_ID("NYK", SAVE_TYPE_EEPROM_4K, FEAT_RPAK), // Yakouchuu II: Satsujin Kouro
MATCH_ID("N3D", SAVE_TYPE_EEPROM_16K, FEAT_RPAK), // Doraemon 3: Nobita no Machi SOS!
MATCH_ID("NB7", SAVE_TYPE_EEPROM_16K, FEAT_RPAK), // Banjo-Tooie [Banjo to Kazooie no Daiboken 2 (J)]
MATCH_ID("NCW", SAVE_TYPE_EEPROM_16K, FEAT_RPAK), // Cruis'n World
MATCH_ID("NCZ", SAVE_TYPE_EEPROM_16K, FEAT_RPAK), // Custom Robo V2
MATCH_ID("ND2", SAVE_TYPE_EEPROM_16K, FEAT_RPAK), // Doraemon 2: Nobita to Hikari no Shinden
MATCH_ID("ND6", SAVE_TYPE_EEPROM_16K, FEAT_RPAK | FEAT_VRU), // Densha de Go! 64
MATCH_ID("NDO", SAVE_TYPE_EEPROM_16K, FEAT_RPAK | FEAT_EXP_PAK_REQUIRED), // Donkey Kong 64
MATCH_ID("NEP", SAVE_TYPE_EEPROM_16K, FEAT_RPAK), // Star Wars Episode I: Racer
MATCH_ID("NEV", SAVE_TYPE_EEPROM_16K, FEAT_RPAK), // Neon Genesis Evangelion
MATCH_ID("NFU", SAVE_TYPE_EEPROM_16K, FEAT_RPAK), // Conker's Bad Fur Day
MATCH_ID("NGC", SAVE_TYPE_EEPROM_16K, FEAT_CPAK | FEAT_RPAK), // GT 64: Championship Edition
MATCH_ID("NGT", SAVE_TYPE_EEPROM_16K, FEAT_CPAK | FEAT_RPAK), // City Tour GrandPrix - Zen Nihon GT Senshuken
MATCH_ID("NIM", SAVE_TYPE_EEPROM_16K, FEAT_NONE), // Ide Yosuke no Mahjong Juku
MATCH_ID("NM8", SAVE_TYPE_EEPROM_16K, FEAT_RPAK | FEAT_TPAK), // Mario Tennis
MATCH_ID("NMV", SAVE_TYPE_EEPROM_16K, FEAT_RPAK), // Mario Party 3
MATCH_ID("NMX", SAVE_TYPE_EEPROM_16K, FEAT_CPAK | FEAT_RPAK), // Excitebike 64
MATCH_ID("NNB", SAVE_TYPE_EEPROM_16K, FEAT_CPAK | FEAT_RPAK), // Kobe Bryant in NBA Courtside
MATCH_ID("NPD", SAVE_TYPE_EEPROM_16K, FEAT_CPAK | FEAT_RPAK | FEAT_TPAK | FEAT_EXP_PAK_RECOMMENDED), // Perfect Dark
MATCH_ID("NPP", SAVE_TYPE_EEPROM_16K, FEAT_CPAK), // Parlor! Pro 64: Pachinko Jikki Simulation Game
MATCH_ID("NR7", SAVE_TYPE_EEPROM_16K, FEAT_TPAK), // Robot Poncots 64: 7tsu no Umi no Caramel
MATCH_ID("NRZ", SAVE_TYPE_EEPROM_16K, FEAT_RPAK), // Ridge Racer 64
MATCH_ID("NUB", SAVE_TYPE_EEPROM_16K, FEAT_CPAK | FEAT_TPAK), // PD Ultraman Battle Collection 64
MATCH_ID("NYS", SAVE_TYPE_EEPROM_16K, FEAT_RPAK), // Yoshi's Story
MATCH_ID("CFZ", SAVE_TYPE_SRAM, FEAT_RPAK | FEAT_64DD_ENHANCED), // F-Zero X (J)
MATCH_ID("CPS", SAVE_TYPE_SRAM, FEAT_TPAK | FEAT_64DD_ENHANCED), // Pocket Monsters Stadium (J)
MATCH_ID("CZL", SAVE_TYPE_SRAM, FEAT_RPAK | FEAT_64DD_ENHANCED), // Legend of Zelda: Ocarina of Time [Zelda no Densetsu - Toki no Ocarina (J)]
MATCH_ID("NA2", SAVE_TYPE_SRAM, FEAT_CPAK | FEAT_RPAK), // Virtual Pro Wrestling 2
MATCH_ID("NAL", SAVE_TYPE_SRAM, FEAT_RPAK), // Super Smash Bros. [Nintendo All-Star! Dairantou Smash Brothers (J)]
MATCH_ID("NB5", SAVE_TYPE_SRAM, FEAT_RPAK), // Biohazard 2 (J)
MATCH_ID("NFZ", SAVE_TYPE_SRAM, FEAT_RPAK), // F-Zero X (U + E)
MATCH_ID("NG6", SAVE_TYPE_SRAM, FEAT_RPAK), // Ganmare Goemon: Dero Dero Douchuu Obake Tenkomori
MATCH_ID("NGP", SAVE_TYPE_SRAM, FEAT_CPAK), // Goemon: Mononoke Sugoroku
MATCH_ID("NHY", SAVE_TYPE_SRAM, FEAT_CPAK | FEAT_RPAK), // Hybrid Heaven (J)
MATCH_ID("NIB", SAVE_TYPE_SRAM, FEAT_RPAK), // Itoi Shigesato no Bass Tsuri No. 1 Kettei Ban!
MATCH_ID("NJ5", SAVE_TYPE_SRAM, FEAT_CPAK), // Jikkyou Powerful Pro Yakyuu 5
MATCH_ID("NJG", SAVE_TYPE_SRAM, FEAT_RPAK), // Jinsei Game 64
MATCH_ID("NKG", SAVE_TYPE_SRAM, FEAT_CPAK | FEAT_RPAK), // Major League Baseball featuring Ken Griffey Jr.
MATCH_ID("NMF", SAVE_TYPE_SRAM, FEAT_RPAK | FEAT_TPAK), // Mario Golf 64
MATCH_ID("NOB", SAVE_TYPE_SRAM, FEAT_NONE), // Ogre Battle 64: Person of Lordly Caliber
MATCH_ID("NP4", SAVE_TYPE_SRAM, FEAT_CPAK), // Jikkyou Powerful Pro Yakyuu 4
MATCH_ID("NP6", SAVE_TYPE_SRAM, FEAT_CPAK | FEAT_TPAK), // Jikkyou Powerful Pro Yakyuu 6
MATCH_ID("NPA", SAVE_TYPE_SRAM, FEAT_CPAK | FEAT_TPAK), // Jikkyou Powerful Pro Yakyuu 2000
MATCH_ID("NPE", SAVE_TYPE_SRAM, FEAT_CPAK), // Jikkyou Powerful Pro Yakyuu Basic Ban 2001
MATCH_ID("NPM", SAVE_TYPE_SRAM, FEAT_CPAK), // Premier Manager 64
MATCH_ID("NPS", SAVE_TYPE_SRAM, FEAT_CPAK | FEAT_RPAK), // Jikkyou J.League 1999: Perfect Striker 2
MATCH_ID("NRE", SAVE_TYPE_SRAM, FEAT_RPAK), // Resident Evil 2
MATCH_ID("NRI", SAVE_TYPE_SRAM, FEAT_CPAK), // New Tetris, The
MATCH_ID("NS4", SAVE_TYPE_SRAM, FEAT_CPAK | FEAT_TPAK), // Super Robot Taisen 64
MATCH_ID("NSI", SAVE_TYPE_SRAM, FEAT_CPAK), // Fushigi no Dungeon: Fuurai no Shiren 2
MATCH_ID("NT3", SAVE_TYPE_SRAM, FEAT_CPAK), // Shin Nihon Pro Wrestling - Toukon Road 2 - The Next Generation (J)
MATCH_ID("NTE", SAVE_TYPE_SRAM, FEAT_RPAK), // 1080 Snowboarding
MATCH_ID("NUM", SAVE_TYPE_SRAM, FEAT_RPAK | FEAT_TPAK), // Nushi Zuri 64: Shiokaze ni Notte
MATCH_ID("NUT", SAVE_TYPE_SRAM, FEAT_CPAK | FEAT_RPAK | FEAT_TPAK), // Nushi Zuri 64
MATCH_ID("NVB", SAVE_TYPE_SRAM, FEAT_RPAK), // Bass Rush - ECOGEAR PowerWorm Championship (J)
MATCH_ID("NVP", SAVE_TYPE_SRAM, FEAT_CPAK | FEAT_RPAK), // Virtual Pro Wrestling 64
MATCH_ID("NW2", SAVE_TYPE_SRAM, FEAT_RPAK), // WCW-nWo Revenge
MATCH_ID("NWL", SAVE_TYPE_SRAM, FEAT_RPAK), // Waialae Country Club: True Golf Classics
MATCH_ID("NWX", SAVE_TYPE_SRAM, FEAT_CPAK | FEAT_RPAK), // WWF WrestleMania 2000
MATCH_ID("NYW", SAVE_TYPE_SRAM, FEAT_NONE), // Harvest Moon 64
MATCH_ID("NZL", SAVE_TYPE_SRAM, FEAT_RPAK), // Legend of Zelda: Ocarina of Time (E)
MATCH_ID("CDZ", SAVE_TYPE_SRAM_BANKED, FEAT_RPAK | FEAT_64DD_ENHANCED), // Dezaemon 3D
MATCH_ID("CP2", SAVE_TYPE_FLASHRAM, FEAT_TPAK | FEAT_64DD_ENHANCED), // Pocket Monsters Stadium 2 (J)
MATCH_ID("NAF", SAVE_TYPE_FLASHRAM, FEAT_CPAK | FEAT_RTC), // Doubutsu no Mori
MATCH_ID("NCC", SAVE_TYPE_FLASHRAM, FEAT_RPAK), // Command & Conquer
MATCH_ID("NCK", SAVE_TYPE_FLASHRAM, FEAT_RPAK), // NBA Courtside 2 featuring Kobe Bryant
MATCH_ID("NDA", SAVE_TYPE_FLASHRAM, FEAT_CPAK), // Derby Stallion 64
MATCH_ID("NDP", SAVE_TYPE_FLASHRAM, FEAT_EXP_PAK_REQUIRED), // Dinosaur Planet (Unlicensed)
MATCH_ID("NJF", SAVE_TYPE_FLASHRAM, FEAT_RPAK), // Jet Force Gemini [Star Twins (J)]
MATCH_ID("NKJ", SAVE_TYPE_FLASHRAM, FEAT_RPAK), // Ken Griffey Jr.'s Slugfest
MATCH_ID("NM6", SAVE_TYPE_FLASHRAM, FEAT_RPAK), // Mega Man 64
MATCH_ID("NMQ", SAVE_TYPE_FLASHRAM, FEAT_RPAK), // Paper Mario
MATCH_ID("NPF", SAVE_TYPE_FLASHRAM, FEAT_NONE), // Pokemon Snap [Pocket Monsters Snap (J)]
MATCH_ID("NPN", SAVE_TYPE_FLASHRAM, FEAT_NONE), // Pokemon Puzzle League
MATCH_ID("NPO", SAVE_TYPE_FLASHRAM, FEAT_TPAK), // Pokemon Stadium
MATCH_ID("NRH", SAVE_TYPE_FLASHRAM, FEAT_RPAK), // Rockman Dash - Hagane no Boukenshin (J)
MATCH_ID("NSQ", SAVE_TYPE_FLASHRAM, FEAT_RPAK | FEAT_EXP_PAK_RECOMMENDED), // StarCraft 64
MATCH_ID("NT9", SAVE_TYPE_FLASHRAM, FEAT_NONE), // Tigger's Honey Hunt
MATCH_ID("NW4", SAVE_TYPE_FLASHRAM, FEAT_CPAK | FEAT_RPAK), // WWF No Mercy
MATCH_ID("NZS", SAVE_TYPE_FLASHRAM, FEAT_RPAK | FEAT_EXP_PAK_REQUIRED), // Legend of Zelda: Majora's Mask [Zelda no Densetsu - Mujura no Kamen (J)]
MATCH_ID("NP3", SAVE_TYPE_FLASHRAM_PKST2, FEAT_TPAK), // Pokemon Stadium 2 [Pocket Monsters Stadium - Kin Gin (J)]
MATCH_ID("N22", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Ready 2 Rumble Boxing - Round 2
MATCH_ID("N2M", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Madden Football 2002
MATCH_ID("N32", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Army Men - Sarge's Heroes 2
MATCH_ID("N3P", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Triple Play 2000
MATCH_ID("N64", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Kira to Kaiketsu! 64 Tanteidan
MATCH_ID("N7I", SAVE_TYPE_NONE, FEAT_CPAK), // FIFA Soccer 64 [FIFA 64 (E)]
MATCH_ID("N8I", SAVE_TYPE_NONE, FEAT_CPAK), // FIFA - Road to World Cup 98 [World Cup e no Michi (J)]
MATCH_ID("N8M", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Madden Football 64
MATCH_ID("N8W", SAVE_TYPE_NONE, FEAT_CPAK), // World Cup '98
MATCH_ID("N9B", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NBA Live '99
MATCH_ID("N9C", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Nascar '99
MATCH_ID("N9F", SAVE_TYPE_NONE, FEAT_CPAK), // FIFA 99
MATCH_ID("N9H", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NHL '99
MATCH_ID("N9M", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Madden Football '99
MATCH_ID("NAC", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Army Men - Air Combat
MATCH_ID("NAH", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Asteroids Hyper 64
MATCH_ID("NAI", SAVE_TYPE_NONE, FEAT_CPAK), // Midway's Greatest Arcade Hits Volume 1
MATCH_ID("NAM", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Army Men - Sarge's Heroes
MATCH_ID("NAR", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Armorines - Project S.W.A.R.M.
MATCH_ID("NAS", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // All-Star Baseball 2001
MATCH_ID("NAY", SAVE_TYPE_NONE, FEAT_CPAK), // Aidyn Chronicles - The First Mage
MATCH_ID("NB2", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NBA In the Zone '99 [NBA Pro '99 (E)]
MATCH_ID("NB3", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Bust-A-Move '99 [Bust-A-Move 3 DX (E)]
MATCH_ID("NB4", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Bass Masters 2000
MATCH_ID("NB8", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Beetle Adventure Racing (J)
MATCH_ID("NB9", SAVE_TYPE_NONE, FEAT_CPAK), // NBA Jam '99
MATCH_ID("NBA", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NBA In the Zone '98 [NBA Pro '98 (E)]
MATCH_ID("NBE", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // All-Star Baseball 2000
MATCH_ID("NBF", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Bio F.R.E.A.K.S.
MATCH_ID("NBI", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NFL Blitz 2000
MATCH_ID("NBJ", SAVE_TYPE_NONE, FEAT_CPAK), // Bakushou Jinsei 64 - Mezase! Resort Ou
MATCH_ID("NBL", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Buck Bumble
MATCH_ID("NBO", SAVE_TYPE_NONE, FEAT_CPAK), // Bottom of the 9th
MATCH_ID("NBP", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Blues Brothers 2000
MATCH_ID("NBQ", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Battletanx - Global Assault
MATCH_ID("NBR", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Milo's Astro Lanes
MATCH_ID("NBS", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // All-Star Baseball '99
MATCH_ID("NBU", SAVE_TYPE_NONE, FEAT_CPAK), // Bust-A-Move 2 - Arcade Edition
MATCH_ID("NBW", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Super Bowling
MATCH_ID("NBX", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Battletanx
MATCH_ID("NBY", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Bug's Life, A
MATCH_ID("NBZ", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NFL Blitz
MATCH_ID("NCB", SAVE_TYPE_NONE, FEAT_RPAK), // Charlie Blast's Territory
MATCH_ID("NCD", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Carmageddon 64
MATCH_ID("NCE", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Nuclear Strike 64
MATCH_ID("NCL", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // California Speed
MATCH_ID("NCO", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Jeremy McGrath Supercross 2000
MATCH_ID("NCS", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // S.C.A.R.S.
MATCH_ID("NDC", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // SD Hiryuu no Ken Densetsu (J)
MATCH_ID("NDE", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Destruction Derby 64
MATCH_ID("NDF", SAVE_TYPE_NONE, FEAT_RPAK), // Dance Dance Revolution - Disney Dancing Museum
MATCH_ID("NDH", SAVE_TYPE_NONE, FEAT_CPAK), // Duel Heroes
MATCH_ID("NDM", SAVE_TYPE_NONE, FEAT_CPAK), // Doom 64
MATCH_ID("NDN", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Duke Nukem 64
MATCH_ID("NDQ", SAVE_TYPE_NONE, FEAT_CPAK), // Disney's Donald Duck - Goin' Quackers [Quack Attack (E)]
MATCH_ID("NDS", SAVE_TYPE_NONE, FEAT_CPAK), // J.League Dynamite Soccer 64
MATCH_ID("NDT", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // South Park
MATCH_ID("NDW", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Daikatana, John Romero's
MATCH_ID("NDZ", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Duke Nukem - Zero Hour
MATCH_ID("NEG", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Extreme-G
MATCH_ID("NET", SAVE_TYPE_NONE, FEAT_CPAK), // Quest 64 [Eltale Monsters (J) Holy Magic Century (E)]
MATCH_ID("NF9", SAVE_TYPE_NONE, FEAT_CPAK), // Fox Sports College Hoops '99
MATCH_ID("NFB", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NFL Blitz 2001
MATCH_ID("NFD", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Flying Dragon
MATCH_ID("NFF", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Fighting Force 64
MATCH_ID("NFL", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Madden Football 2001
MATCH_ID("NFO", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Forsaken 64
MATCH_ID("NFQ", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Razor Freestyle Scooter
MATCH_ID("NFR", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // F-1 Racing Championship
MATCH_ID("NFS", SAVE_TYPE_NONE, FEAT_CPAK), // Famista 64
MATCH_ID("NG2", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Extreme-G XG2
MATCH_ID("NG5", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Ganbare Goemon - Neo Momoyama Bakufu no Odori [Mystical Ninja Starring Goemon]
MATCH_ID("NGA", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Deadly Arts [G.A.S.P!! Fighter's NEXTream (E-J)]
MATCH_ID("NGB", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Top Gear Hyper Bike
MATCH_ID("NGD", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Gauntlet Legends (J)
MATCH_ID("NGM", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Goemon's Great Adventure [Mystical Ninja 2 Starring Goemon]
MATCH_ID("NGN", SAVE_TYPE_NONE, FEAT_CPAK), // Golden Nugget 64
MATCH_ID("NGR", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Top Gear Rally (U)
MATCH_ID("NGS", SAVE_TYPE_NONE, FEAT_CPAK), // Jikkyou G1 Stable
MATCH_ID("NGX", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Gauntlet Legends
MATCH_ID("NH5", SAVE_TYPE_NONE, FEAT_CPAK), // Nagano Winter Olympics '98 [Hyper Olympics in Nagano (J)]
MATCH_ID("NH9", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NHL Breakaway '99
MATCH_ID("NHC", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Hercules - The Legendary Journeys
MATCH_ID("NHG", SAVE_TYPE_NONE, FEAT_CPAK), // F-1 Pole Position 64
MATCH_ID("NHG", SAVE_TYPE_NONE, FEAT_CPAK), // Human Grand Prix - New Generation
MATCH_ID("NHK", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Hiryuu no Ken Twin
MATCH_ID("NHL", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NHL Breakaway '98
MATCH_ID("NHM", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Mia Hamm Soccer 64
MATCH_ID("NHN", SAVE_TYPE_NONE, FEAT_CPAK), // Olympic Hockey Nagano '98
MATCH_ID("NHO", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NHL Blades of Steel '99 [NHL Pro '99 (E)]
MATCH_ID("NHS", SAVE_TYPE_NONE, FEAT_CPAK), // Hamster Monogatari 64
MATCH_ID("NHT", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Hydro Thunder
MATCH_ID("NHV", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Hybrid Heaven (U + E)
MATCH_ID("NHW", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Hot Wheels Turbo Racing
MATCH_ID("NHX", SAVE_TYPE_NONE, FEAT_CPAK), // Hexen
MATCH_ID("NIS", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // International Superstar Soccer 2000
MATCH_ID("NIV", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Space Invaders
MATCH_ID("NJ2", SAVE_TYPE_NONE, FEAT_CPAK), // Wonder Project J2 - Koruro no Mori no Jozet (J)
MATCH_ID("NJ3", SAVE_TYPE_NONE, FEAT_CPAK), // Jikkyou World Soccer 3
MATCH_ID("NJA", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NBA Jam 2000
MATCH_ID("NJE", SAVE_TYPE_NONE, FEAT_CPAK), // J.League Eleven Beat 1997
MATCH_ID("NJL", SAVE_TYPE_NONE, FEAT_CPAK), // J.League Live 64
MATCH_ID("NJP", SAVE_TYPE_NONE, FEAT_CPAK), // International Superstar Soccer 64 [Jikkyo J-League Perfect Striker (J)]
MATCH_ID("NJQ", SAVE_TYPE_NONE, FEAT_RPAK), // Batman Beyond - Return of the Joker [Batman of the Future - Return of the Joker (E)]
MATCH_ID("NKE", SAVE_TYPE_NONE, FEAT_RPAK), // Knife Edge - Nose Gunner
MATCH_ID("NKK", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Knockout Kings 2000
MATCH_ID("NKM", SAVE_TYPE_NONE, FEAT_CPAK), // Pro Mahjong Kiwame 64 (J)
MATCH_ID("NKR", SAVE_TYPE_NONE, FEAT_CPAK), // Rakuga Kids (E)
MATCH_ID("NL2", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Top Gear Rally 2
MATCH_ID("NLC", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Automobili Lamborghini [Super Speed Race 64 (J)]
MATCH_ID("NLG", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // LEGO Racers
MATCH_ID("NM3", SAVE_TYPE_NONE, FEAT_RPAK), // Monster Truck Madness 64
MATCH_ID("NM4", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Mortal Kombat 4
MATCH_ID("NM9", SAVE_TYPE_NONE, FEAT_CPAK), // Harukanaru Augusta Masters 98
MATCH_ID("NMA", SAVE_TYPE_NONE, FEAT_CPAK), // Jangou Simulation Mahjong Do 64
MATCH_ID("NMB", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Mike Piazza's Strike Zone
MATCH_ID("NMD", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Madden Football 2000
MATCH_ID("NMJ", SAVE_TYPE_NONE, FEAT_CPAK), // Mahjong 64
MATCH_ID("NMM", SAVE_TYPE_NONE, FEAT_CPAK), // Mahjong Master
MATCH_ID("NMT", SAVE_TYPE_NONE, FEAT_RPAK), // Magical Tetris Challenge
MATCH_ID("NMY", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Mortal Kombat Mythologies - Sub-Zero
MATCH_ID("NN2", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Nascar 2000
MATCH_ID("NNC", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Nightmare Creatures
MATCH_ID("NNL", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NBA Live 2000
MATCH_ID("NNM", SAVE_TYPE_NONE, FEAT_CPAK), // Namco Museum 64
MATCH_ID("NNR", SAVE_TYPE_NONE, FEAT_CPAK), // Pro Mahjong Tsuwamono 64 - Jansou Battle ni Chousen (J)
MATCH_ID("NNS", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Beetle Adventure Racing
MATCH_ID("NO7", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // The World Is Not Enough
MATCH_ID("NOF", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Offroad Challenge
MATCH_ID("NOH", SAVE_TYPE_NONE, FEAT_RPAK | FEAT_TPAK), // Transformers Beast Wars - Transmetals
MATCH_ID("NOM", SAVE_TYPE_NONE, FEAT_CPAK), // Onegai Monsters
MATCH_ID("NOW", SAVE_TYPE_NONE, FEAT_CPAK), // Brunswick Circuit Pro Bowling
MATCH_ID("NP9", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Ms. Pac-Man - Maze Madness
MATCH_ID("NPB", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Puzzle Bobble 64 (J)
MATCH_ID("NPC", SAVE_TYPE_NONE, FEAT_CPAK), // Pachinko 365 Nichi (J)
MATCH_ID("NPK", SAVE_TYPE_NONE, FEAT_CPAK), // Chou Kuukan Night Pro Yakyuu King (J)
MATCH_ID("NPL", SAVE_TYPE_NONE, FEAT_CPAK), // Power League 64 (J)
MATCH_ID("NPR", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // South Park Rally
MATCH_ID("NPU", SAVE_TYPE_NONE, FEAT_CPAK), // Power Rangers - Lightspeed Rescue
MATCH_ID("NPX", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Polaris SnoCross
MATCH_ID("NPZ", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Susume! Taisen Puzzle Dama Toukon! Marumata Chou (J)
MATCH_ID("NQ2", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Quake 2
MATCH_ID("NQ8", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NFL Quarterback Club '98
MATCH_ID("NQ9", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NFL Quarterback Club '99
MATCH_ID("NQB", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NFL Quarterback Club 2000
MATCH_ID("NQC", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NFL Quarterback Club 2001
MATCH_ID("NQK", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Quake 64
MATCH_ID("NR2", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Rush 2 - Extreme Racing USA
MATCH_ID("NR3", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Stunt Racer 64
MATCH_ID("NR6", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Tom Clancy's Rainbow Six
MATCH_ID("NRD", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Ready 2 Rumble Boxing
MATCH_ID("NRG", SAVE_TYPE_NONE, FEAT_RPAK), // Rugrats - Scavenger Hunt [Treasure Hunt (E)]
MATCH_ID("NRK", SAVE_TYPE_NONE, FEAT_CPAK), // Rugrats in Paris - The Movie
MATCH_ID("NRO", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Road Rash 64
MATCH_ID("NRP", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Rampage - World Tour
MATCH_ID("NRP", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Rampage 2 - Universal Tour
MATCH_ID("NRR", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Roadster's Trophy
MATCH_ID("NRT", SAVE_TYPE_NONE, FEAT_CPAK), // Rat Attack
MATCH_ID("NRT", SAVE_TYPE_NONE, FEAT_CPAK), // Robotron 64
MATCH_ID("NRU", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // San Francisco Rush 2049
MATCH_ID("NRV", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Re-Volt
MATCH_ID("NRW", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Turok: Rage Wars
MATCH_ID("NS2", SAVE_TYPE_NONE, FEAT_CPAK), // Simcity 2000
MATCH_ID("NSB", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Twisted Edge - Extreme Snowboarding [King Hill 64 - Extreme Snowboarding (J)]
MATCH_ID("NSD", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Shadow Man
MATCH_ID("NSF", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // San Francisco Rush - Extreme Racing
MATCH_ID("NSG", SAVE_TYPE_NONE, FEAT_CPAK), // Shadowgate 64 - Trials Of The Four Towers
MATCH_ID("NSH", SAVE_TYPE_NONE, FEAT_CPAK), // Saikyou Habu Shougi (J)
MATCH_ID("NSK", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Snowboard Kids [Snobow Kids (J)]
MATCH_ID("NSL", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Spider-Man
MATCH_ID("NSO", SAVE_TYPE_NONE, FEAT_CPAK), // NBA Showtime - NBA on NBC
MATCH_ID("NSP", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Superman
MATCH_ID("NST", SAVE_TYPE_NONE, FEAT_CPAK), // Eikou no Saint Andrews
MATCH_ID("NSX", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Supercross 2000
MATCH_ID("NSY", SAVE_TYPE_NONE, FEAT_CPAK), // Scooby-Doo! - Classic Creep Capers
MATCH_ID("NSZ", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NFL Blitz - Special Edition
MATCH_ID("NT2", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Turok 2 - Seeds of Evil [Violence Killer - Turok New Generation (J)]
MATCH_ID("NT3", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Tony Hawk's Pro Skater 3
MATCH_ID("NT4", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // CyberTiger
MATCH_ID("NTA", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Disney's Tarzan
MATCH_ID("NTF", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Tony Hawk's Pro Skater
MATCH_ID("NTH", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Toy Story 2 - Buzz Lightyear to the Rescue!
MATCH_ID("NTI", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // WWF: Attitude
MATCH_ID("NTK", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Turok 3 - Shadow of Oblivion
MATCH_ID("NTO", SAVE_TYPE_NONE, FEAT_CPAK), // Shin Nihon Pro Wrestling - Toukon Road - Brave Spirits (J)
MATCH_ID("NTQ", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Tony Hawk's Pro Skater 2
MATCH_ID("NTS", SAVE_TYPE_NONE, FEAT_CPAK), // Centre Court Tennis [Let's Smash (J)]
MATCH_ID("NTT", SAVE_TYPE_NONE, FEAT_CPAK), // Tonic Trouble
MATCH_ID("NTU", SAVE_TYPE_NONE, FEAT_CPAK), // Turok: Dinosaur Hunter [Turok: Jikuu Senshi (J)]
MATCH_ID("NV2", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Chameleon Twist 2
MATCH_ID("NV3", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Micro Machines 64 Turbo
MATCH_ID("NV8", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Vigilante 8
MATCH_ID("NVC", SAVE_TYPE_NONE, FEAT_CPAK), // Virtual Chess 64
MATCH_ID("NVG", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Vigilante 8 - Second Offense
MATCH_ID("NVR", SAVE_TYPE_NONE, FEAT_CPAK), // Virtual Pool 64
MATCH_ID("NW3", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // WCW: Nitro
MATCH_ID("NW8", SAVE_TYPE_NONE, FEAT_CPAK), // Wayne Gretzky's 3D Hockey '98
MATCH_ID("NWB", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Iggy's Reckin' Balls [Iggy-kun no Bura Bura Poyon (J)]
MATCH_ID("NWD", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Winback - Covert Operations
MATCH_ID("NWF", SAVE_TYPE_NONE, FEAT_RPAK), // Wheel of Fortune
MATCH_ID("NWG", SAVE_TYPE_NONE, FEAT_CPAK), // Wayne Gretzky's 3D Hockey
MATCH_ID("NWI", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // ECW Hardcore Revolution
MATCH_ID("NWK", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Michael Owens WLS 2000 [World League Soccer 2000 (E) / Telefoot Soccer 2000 (F)]
MATCH_ID("NWM", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // WCW: Mayhem
MATCH_ID("NWN", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // WCW vs. nWo - World Tour
MATCH_ID("NWO", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // World Driver Championship
MATCH_ID("NWP", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Wipeout 64
MATCH_ID("NWS", SAVE_TYPE_NONE, FEAT_CPAK), // International Superstar Soccer '98 [Jikkyo World Soccer - World Cup France '98 (J)]
MATCH_ID("NWV", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // WCW: Backstage Assault
MATCH_ID("NWW", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // WWF: War Zone
MATCH_ID("NWZ", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // NBA In the Zone 2000
MATCH_ID("NX2", SAVE_TYPE_NONE, FEAT_CPAK), // Gex 64 - Enter the Gecko
MATCH_ID("NX3", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Gex 3 - Deep Cover Gecko
MATCH_ID("NXF", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Xena Warrior Princess - The Talisman of Fate
MATCH_ID("NXG", SAVE_TYPE_NONE, FEAT_CPAK), // NBA Hangtime
MATCH_ID("NY2", SAVE_TYPE_NONE, FEAT_CPAK), // Rayman 2 - The Great Escape
MATCH_ID("NYP", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Paperboy
MATCH_ID("NYW", SAVE_TYPE_NONE, FEAT_CPAK), // Bokujou Monogatari 2
MATCH_ID("NZO", SAVE_TYPE_NONE, FEAT_CPAK | FEAT_RPAK), // Battlezone - Rise of the Black Dogs
MATCH_END,
};
static void fix_rom_header_endianness (rom_header_t *rom_header, rom_info_t *rom_info) {
uint8_t *raw = (uint8_t *) (rom_header);
switch (rom_header->pi_dom1_config) {
case PI_CONFIG_LITTLE_ENDIAN:
rom_info->endianness = ENDIANNESS_LITTLE;
for (int i = 0; i < sizeof(rom_header_t); i += 4) {
SWAP_VARS(raw[i + 0], raw[i + 3]);
SWAP_VARS(raw[i + 1], raw[i + 2]);
}
break;
case PI_CONFIG_BYTE_SWAPPED:
rom_info->endianness = ENDIANNESS_BYTE_SWAP;
for (int i = 0; i < sizeof(rom_header_t); i += 2) {
SWAP_VARS(raw[i + 0], raw[i + 1]);
}
break;
default:
rom_info->endianness = ENDIANNESS_BIG;
break;
}
}
static bool compare_id (const match_t *match, rom_header_t *rom_header) {
int characters_to_check = (match->type == MATCH_TYPE_ID) ? 3 : 4;
for (int i = (characters_to_check - 1); i >= 0; i--) {
if (match->fields.id[i] != rom_header->game_code[i]) {
return false;
}
}
if (match->type == MATCH_TYPE_ID_REGION_VERSION) {
if (match->fields.version != rom_header->version) {
return false;
}
}
return true;
}
static match_t find_rom_in_database (rom_header_t *rom_header) {
const match_t *match;
for (match = database; match->type != MATCH_TYPE_END; match++) {
switch (match->type) {
case MATCH_TYPE_ID:
case MATCH_TYPE_ID_REGION:
case MATCH_TYPE_ID_REGION_VERSION:
if (compare_id(match, rom_header)) {
return *match;
}
break;
case MATCH_TYPE_CHECK_CODE:
if (match->fields.check_code == rom_header->check_code) {
return *match;
}
break;
case MATCH_TYPE_HOMEBREW_HEADER:
if (strncmp(match->fields.id, rom_header->unique_code, sizeof(rom_header->unique_code)) == 0) {
return *match;
}
break;
default:
break;
}
}
return *match;
}
static uint32_t fix_boot_address (cic_type_t cic_type, uint32_t boot_address) {
switch (cic_type) {
case CIC_x103: return (boot_address - 0x100000);
case CIC_x106: return (boot_address - 0x200000);
default: return boot_address;
}
}
static void extract_rom_info (match_t *match, rom_header_t *rom_header, rom_info_t *rom_info) {
rom_info->cic_type = cic_detect(rom_header->ipl3);
if (match->type == MATCH_TYPE_HOMEBREW_HEADER) {
if (rom_header->version & (1 << 0)) {
match->data.feat |= FEAT_RTC;
}
switch ((rom_header->version & 0xF0) >> 4) {
case 0: match->data.save = SAVE_TYPE_NONE; break;
case 1: match->data.save = SAVE_TYPE_EEPROM_4K; break;
case 2: match->data.save = SAVE_TYPE_EEPROM_16K; break;
case 3: match->data.save = SAVE_TYPE_SRAM; break;
case 4: match->data.save = SAVE_TYPE_SRAM_BANKED; break;
case 5: match->data.save = SAVE_TYPE_FLASHRAM; break;
case 6: match->data.save = SAVE_TYPE_SRAM_128K; break;
default: match->data.save = SAVE_TYPE_NONE; break;
}
}
rom_info->clock_rate = (rom_header->clock_rate == CLOCK_RATE_DEFAULT) ? 62.5f : (rom_header->clock_rate & ~(CLOCK_RATE_DEFAULT)) / 1000000.0f;
rom_info->boot_address = fix_boot_address(rom_info->cic_type, rom_header->boot_address);
rom_info->libultra.version = rom_header->libultra.version;
rom_info->libultra.revision = rom_header->libultra.revision;
rom_info->check_code = rom_header->check_code;
memcpy(rom_info->title, rom_header->title, sizeof(rom_info->title));
memcpy(rom_info->game_code, rom_header->game_code, sizeof(rom_info->game_code));
rom_info->version = rom_header->version;
rom_info->save_type = match->data.save;
rom_info->features.controller_pak = (match->data.feat & FEAT_CPAK);
rom_info->features.rumble_pak = (match->data.feat & FEAT_RPAK);
rom_info->features.transfer_pak = (match->data.feat & FEAT_TPAK);
rom_info->features.voice_recognition_unit = (match->data.feat & FEAT_VRU);
rom_info->features.real_time_clock = (match->data.feat & FEAT_RTC);
rom_info->features.disk_conversion = (match->data.feat & FEAT_64DD_CONVERSION);
rom_info->features.combo_rom_disk_game = (match->data.feat & FEAT_64DD_ENHANCED);
if (match->data.feat & FEAT_EXP_PAK_REQUIRED) {
rom_info->features.expansion_pak = EXPANSION_PAK_REQUIRED;
} else if (match->data.feat & FEAT_EXP_PAK_RECOMMENDED) {
rom_info->features.expansion_pak = EXPANSION_PAK_RECOMMENDED;
} else if (match->data.feat & FEAT_EXP_PAK_ENHANCED) {
rom_info->features.expansion_pak = EXPANSION_PAK_SUGGESTED;
} else if (match->data.feat & FEAT_EXP_PAK_BROKEN) {
rom_info->features.expansion_pak = EXPANSION_PAK_FAULTY;
} else {
rom_info->features.expansion_pak = EXPANSION_PAK_NONE;
}
}
rom_err_t rom_info_load (char *path, rom_info_t *rom_info) {
FIL fil;
UINT br;
rom_header_t rom_header;
if (f_open(&fil, strip_sd_prefix(path), FA_READ) != FR_OK) {
return ROM_ERR_NO_FILE;
}
if (f_read(&fil, &rom_header, sizeof(rom_header), &br) != FR_OK) {
f_close(&fil);
return ROM_ERR_IO;
}
if (f_close(&fil) != FR_OK) {
return ROM_ERR_IO;
}
if (br != sizeof(rom_header)) {
return ROM_ERR_IO;
}
fix_rom_header_endianness(&rom_header, rom_info);
match_t match = find_rom_in_database(&rom_header);
extract_rom_info(&match, &rom_header, rom_info);
return ROM_OK;
}

156
src/menu/rom_info.h Normal file
View File

@ -0,0 +1,156 @@
/**
* @file rom_info.h
* @brief N64 ROM Database.
* @note Only works with N64 ROM's by checking the first 1024 bytes of the file.
* @ingroup menu
*/
#ifndef ROM_INFO_H__
#define ROM_INFO_H__
#include <stdbool.h>
#include <stdint.h>
#include "boot/cic.h"
typedef enum {
ROM_OK,
ROM_ERR_IO,
ROM_ERR_NO_FILE,
} rom_err_t;
typedef enum {
ENDIANNESS_BIG,
ENDIANNESS_LITTLE,
ENDIANNESS_BYTE_SWAP,
} endianness_t;
/** @brief ROM media type enumeration. */
typedef enum {
/** @brief Is a stand alone Cartridge program. */
N64_CART = 'N',
/** @brief Is a stand alone Disk Drive program. */
N64_DISK = 'D',
/** @brief Is a Cartridge program that could use an extra Disk Drive program to expand its capabilities. */
N64_CART_EXPANDABLE = 'C',
/** @brief Is a Disk Drive program that could use an extra Cartridge program to expand its capabilities. */
N64_DISK_EXPANDABLE = 'E',
/** @brief Is an Aleck64 program. */
N64_ALECK64 = 'Z'
} category_type_t;
/** @brief ROM market region & language type enumeration. */
typedef enum {
/** @brief The ROM is designed for Japanese and "English" languages. */
MARKET_JAPANESE_MULTI = 'A', // 1080 Snowboarding JPN is the only ROM that uses this? possibily a mistake, or the fact it also includes American English!.
/** @brief The ROM is designed for Brazil (Portuguese) language. */
MARKET_BRAZILIAN = 'B',
/** @brief The ROM is designed for Chinese language. */
MARKET_CHINESE = 'C',
/** @brief The ROM is designed for German language. */
MARKET_GERMAN = 'D',
/** @brief The ROM is designed for North American "English" language. */
MARKET_NORTH_AMERICA = 'E',
/** @brief The ROM is designed for French language. */
MARKET_FRENCH = 'F',
/** @brief The ROM is designed for a NTSC Gateway 64. */
MARKET_GATEWAY64_NTSC = 'G',
/** @brief The ROM is designed for Dutch language. */
MARKET_DUTCH = 'H',
/** @brief The ROM is designed for Italian language. */
MARKET_ITALIAN = 'I',
/** @brief The ROM is designed for Japanese language. */
MARKET_JAPANESE = 'J',
/** @brief The ROM is designed for Korean language. */
MARKET_KOREAN = 'K',
/** @brief The ROM is designed for a PAL Gateway 64. */
MARKET_GATEWAY64_PAL = 'L',
/** @brief The ROM is designed for Canada region (English and French) language. */
MARKET_CANADIAN = 'N',
/** @brief The ROM is designed for European market and languages (must at minimum include English). */
MARKET_EUROPEAN_BASIC = 'P', // Sometimes used for Australian region ROMs as well.
/** @brief The ROM is designed for Spanish language */
MARKET_SPANISH = 'S',
/** @brief The ROM is designed for Australia (English) language. */
MARKET_AUSTRALIAN = 'U',
/** @brief The ROM is designed for Scandinavian (Swedish, Norwegian, Finnish, etc.) languages. */
MARKET_SCANDINAVIAN = 'W',
/** @brief The ROM is designed for an undefined region and TBD language(s). */
MARKET_OTHER_X = 'X', // many EU ROM's, Top Gear Rally (Asia) and HSV Racing (AUS) ROM uses this.
/** @brief The ROM is designed for a European region and language(s). */
MARKET_OTHER_Y = 'Y', // many EU ROM's uses this.
/** @brief The ROM is designed for an undefined region and TBD language(s). */
MARKET_OTHER_Z = 'Z' // no known ROM's use this.
} destination_type_t;
typedef enum {
SAVE_TYPE_NONE,
SAVE_TYPE_EEPROM_4K,
SAVE_TYPE_EEPROM_16K,
SAVE_TYPE_SRAM,
SAVE_TYPE_SRAM_BANKED,
SAVE_TYPE_SRAM_128K,
SAVE_TYPE_FLASHRAM,
SAVE_TYPE_FLASHRAM_PKST2,
} save_type_t;
typedef enum {
/** @brief The ROM is happy with 4MB of memory. */
EXPANSION_PAK_NONE,
/** @brief The ROM requires 8MB of memory. */
EXPANSION_PAK_REQUIRED,
/** @brief The ROM recommends 8MB of memory. */
EXPANSION_PAK_RECOMMENDED,
/** @brief The ROM suggests 8MB of memory. */
EXPANSION_PAK_SUGGESTED,
/** @brief The ROM is faulty when using 8MB of memory. */
EXPANSION_PAK_FAULTY,
} expansion_pak_t;
typedef struct {
endianness_t endianness;
float clock_rate;
uint32_t boot_address;
struct {
uint8_t version;
char revision;
} libultra;
uint64_t check_code;
char title[20];
union {
char game_code[4];
struct {
category_type_t category_code : 8;
char unique_code[2];
destination_type_t destination_code : 8;
};
};
uint8_t version;
cic_type_t cic_type;
save_type_t save_type;
struct {
bool controller_pak;
bool rumble_pak;
bool transfer_pak;
bool voice_recognition_unit;
bool real_time_clock;
bool disk_conversion;
bool combo_rom_disk_game;
expansion_pak_t expansion_pak;
} features;
} rom_info_t;
rom_err_t rom_info_load (char *path, rom_info_t *rom_info);
#endif

View File

@ -4,15 +4,20 @@
#include "settings.h"
#include "utils/fs.h"
#define SETTINGS_FILE_PATH "sd:/config.ini"
#ifndef SETTINGS_FILE_PATH
#define SETTINGS_FILE_PATH "sd:/menu/config.ini"
#endif
static settings_t init = {
.pal60 = false,
.show_hidden_files = false,
.pal60_enabled = false,
.hidden_files_enabled = false,
.default_directory = "/",
.use_saves_folder = false,
.use_saves_folder = true,
/* Beta feature flags */
.bgm_enabled = false,
.sound_enabled = false,
.rumble_enabled = true,
};
@ -23,22 +28,33 @@ void settings_load (settings_t *settings) {
mini_t *ini = mini_try_load(SETTINGS_FILE_PATH);
settings->pal60 = mini_get_bool(ini, "menu", "pal60", init.pal60);
settings->show_hidden_files = mini_get_bool(ini, "menu", "show_hidden_files", init.show_hidden_files);
settings->pal60_enabled = mini_get_bool(ini, "menu", "pal60", init.pal60_enabled); // TODO: consider changing file setting name
settings->hidden_files_enabled = mini_get_bool(ini, "menu", "show_hidden_files", init.hidden_files_enabled);
settings->default_directory = strdup(mini_get_string(ini, "menu", "default_directory", init.default_directory));
settings->use_saves_folder = mini_get_bool(ini, "menu", "use_saves_folder", init.use_saves_folder);
/* Beta feature flags, they might not be in the file */
settings->bgm_enabled = mini_get_bool(ini, "menu_beta_flag", "bgm_enabled", init.bgm_enabled);
settings->sound_enabled = mini_get_bool(ini, "menu_beta_flag", "sound_enabled", init.sound_enabled);
settings->rumble_enabled = mini_get_bool(ini, "menu_beta_flag", "rumble_enabled", init.rumble_enabled);
mini_free(ini);
}
void settings_save (settings_t *settings) {
mini_t *ini = mini_create(SETTINGS_FILE_PATH);
mini_set_bool(ini, "menu", "pal60", settings->pal60);
mini_set_bool(ini, "menu", "show_hidden_files", settings->show_hidden_files);
mini_set_bool(ini, "menu", "pal60", settings->pal60_enabled);
mini_set_bool(ini, "menu", "show_hidden_files", settings->hidden_files_enabled);
mini_set_string(ini, "menu", "default_directory", settings->default_directory);
mini_set_bool(ini, "menu", "use_saves_folder", settings->use_saves_folder);
/* Beta feature flags, they should not save until production ready! */
// mini_set_bool(ini, "menu_beta_flag", "bgm_enabled", init.bgm_enabled);
// mini_set_bool(ini, "menu_beta_flag", "sound_enabled", init.sound_enabled);
// mini_set_bool(ini, "menu_beta_flag", "rumble_enabled", init.rumble_enabled);
mini_save(ini);
mini_free(ini);

View File

@ -10,17 +10,27 @@
/** @brief Settings Structure */
typedef struct {
/** @brief Select 60 Hz refresh rate if running on PAL console */
bool pal60;
/** @brief Use 60 Hz refresh rate on a PAL console */
bool pal60_enabled;
/** @brief Show files marked as hidden in the browser */
bool show_hidden_files;
bool hidden_files_enabled;
/** @brief Default directory to navigate to when menu loads */
char *default_directory;
/** @brief Put saves into separate directory */
bool use_saves_folder;
/** @brief Enable Background music */
bool bgm_enabled;
/** @brief Enable Sounds */
bool sound_enabled;
/** @brief Enable rumble feedback */
bool rumble_enabled;
} settings_t;

View File

@ -35,6 +35,14 @@ void sound_init_mp3_playback (void) {
sound_reconfigure(mp3player_get_samplerate());
}
void sound_deinit (void) {
if (sound_initialized) {
mixer_close();
audio_close();
sound_initialized = false;
}
}
void sound_poll (void) {
if (sound_initialized && audio_can_write()) {
short *audio_buffer = audio_write_begin();
@ -42,11 +50,3 @@ void sound_poll (void) {
audio_write_end();
}
}
void sound_close (void) {
if (sound_initialized) {
mixer_close();
audio_close();
sound_initialized = false;
}
}

View File

@ -13,8 +13,8 @@
void sound_init_default (void);
void sound_init_mp3_playback (void);
void sound_deinit (void);
void sound_poll (void);
void sound_close (void);
#endif

View File

@ -10,6 +10,7 @@
static const char *rom_extensions[] = { "z64", "n64", "v64", "rom", NULL };
static const char *disk_extensions[] = { "ndd", NULL };
static const char *emulator_extensions[] = { "nes", "sfc", "smc", "gb", "gbc", "sms", "gg", "sg", NULL };
static const char *save_extensions[] = { "sav", NULL }; // TODO: "eep", "sra", "srm", "fla" could be used if transfered from different flashcarts.
static const char *image_extensions[] = { "png", NULL };
@ -29,6 +30,10 @@ static int compare_entry (const void *pa, const void *pb) {
return -1;
} else if (b->type == ENTRY_TYPE_ROM) {
return 1;
} else if (a->type == ENTRY_TYPE_DISK) {
return -1;
} else if (b->type == ENTRY_TYPE_DISK) {
return 1;
} else if (a->type == ENTRY_TYPE_EMULATOR) {
return -1;
} else if (b->type == ENTRY_TYPE_EMULATOR) {
@ -81,7 +86,7 @@ static bool load_directory (menu_t *menu) {
if (info.fattrib & AM_SYS) {
continue;
}
if ((info.fattrib & AM_HID) && !menu->settings.show_hidden_files) {
if ((info.fattrib & AM_HID) && !menu->settings.hidden_files_enabled) {
continue;
}
@ -97,6 +102,8 @@ static bool load_directory (menu_t *menu) {
entry->type = ENTRY_TYPE_DIR;
} else if (file_has_extensions(info.fname, rom_extensions)) {
entry->type = ENTRY_TYPE_ROM;
} else if (file_has_extensions(info.fname, disk_extensions)) {
entry->type = ENTRY_TYPE_DISK;
}else if (file_has_extensions(info.fname, emulator_extensions)) {
entry->type = ENTRY_TYPE_EMULATOR;
} else if (file_has_extensions(info.fname, save_extensions)) {
@ -226,7 +233,7 @@ static void process (menu_t *menu) {
return;
}
int scroll_speed = menu->actions.fast ? 10 : 1;
int scroll_speed = menu->actions.go_fast ? 10 : 1;
if (menu->browser.entries > 1) {
if (menu->actions.go_up) {
@ -254,6 +261,9 @@ static void process (menu_t *menu) {
case ENTRY_TYPE_ROM:
menu->next_mode = MENU_MODE_LOAD_ROM;
break;
case ENTRY_TYPE_DISK:
menu->next_mode = MENU_MODE_LOAD_DISK;
break;
case ENTRY_TYPE_EMULATOR:
menu->next_mode = MENU_MODE_LOAD_EMULATOR;
break;
@ -297,6 +307,7 @@ static void draw (menu_t *menu, surface_t *d) {
switch (menu->browser.entry->type) {
case ENTRY_TYPE_DIR: action = "A: Enter"; break;
case ENTRY_TYPE_ROM: action = "A: Load"; break;
case ENTRY_TYPE_DISK: action = "A: Load"; break;
case ENTRY_TYPE_IMAGE: action = "A: Show"; break;
case ENTRY_TYPE_MUSIC: action = "A: Play"; break;
default: action = "A: Info"; break;

View File

@ -23,11 +23,18 @@ static void draw (menu_t *menu, surface_t *d) {
static void deinit (menu_t *menu) {
menu->error_message = NULL;
menu->flashcart_err = FLASHCART_OK;
}
void view_error_init (menu_t *menu) {
// Nothing to initialize (yet)
if (menu->flashcart_err != FLASHCART_OK) {
debugf(
"Flashcart error [%d]: %s\n",
menu->flashcart_err,
flashcart_convert_error_message(menu->flashcart_err)
);
}
}
void view_error_display (menu_t *menu, surface_t *display) {

View File

@ -1,38 +1,16 @@
#include "views.h"
static char *convert_error_message (flashcart_error_t error) {
switch (error) {
case FLASHCART_OK:
return "No error";
case FLASHCART_ERROR_NOT_DETECTED:
return "No flashcart hardware was detected";
case FLASHCART_ERROR_OUTDATED:
return "Outdated flashcart firmware";
case FLASHCART_ERROR_SD_CARD:
return "Error during SD card initialization";
case FLASHCART_ERROR_ARGS:
return "Invalid argument passed to flashcart function";
case FLASHCART_ERROR_LOAD:
return "Error during loading data into flashcart";
case FLASHCART_ERROR_INT:
return "Internal flashcart error";
default:
return "Unknown flashcart error";
}
}
static void draw (menu_t *menu, surface_t *d) {
rdpq_attach(d, NULL);
rdpq_clear(RGBA32(0x7F, 0x00, 0x00, 0xFF));
const char *firmware_message = (
"Minimum supported versions:\n"
"EverDrive-64: ?\n"
"64drive: 2.05\n"
"SC64: 2.16.0"
"Supported firmware versions:\n"
"64drive: 2.05+\n"
"EverDrive-64: ???+\n"
"SummerCart64: 2.17.0+"
);
component_messagebox_draw(
@ -41,8 +19,8 @@ static void draw (menu_t *menu, surface_t *d) {
"%s\n"
"\n"
"%s",
convert_error_message(menu->flashcart_error),
menu->flashcart_error == FLASHCART_ERROR_OUTDATED ? firmware_message : ""
flashcart_convert_error_message(menu->flashcart_err),
(menu->flashcart_err == FLASHCART_ERR_OUTDATED) ? firmware_message : ""
);
rdpq_detach_show();

160
src/menu/views/load_disk.c Normal file
View File

@ -0,0 +1,160 @@
#include "../cart_load.h"
#include "../disk_info.h"
#include "boot/boot.h"
#include "views.h"
static bool load_pending;
static bool load_rom;
static char *convert_error_message (disk_err_t err) {
switch (err) {
case DISK_ERR_IO: return "I/O error during loading 64DD disk information";
case DISK_ERR_NO_FILE: return "Couldn't open 64DD disk file";
case DISK_ERR_INVALID: return "Invalid 64DD disk file";
default: return "Unknown disk info load error";
}
}
static char *format_disk_region (disk_region_t region) {
switch (region) {
case DISK_REGION_DEVELOPMENT: return "Development";
case DISK_REGION_JAPANESE: return "Japan";
case DISK_REGION_USA: return "USA";
default: return "Unknown";
}
}
static void process (menu_t *menu) {
if (menu->actions.enter) {
load_pending = true;
load_rom = false;
} else if (menu->actions.options && menu->load.rom_path) {
load_pending = true;
load_rom = true;
} else if (menu->actions.back) {
menu->next_mode = MENU_MODE_BROWSER;
}
}
static void draw (menu_t *menu, surface_t *d) {
rdpq_attach(d, NULL);
component_background_draw();
if (load_pending) {
component_loader_draw(0.0f);
} else {
component_layout_draw();
component_main_text_draw(
ALIGN_CENTER, VALIGN_TOP,
"64DD disk information\n"
"\n"
"%s",
menu->browser.entry->name
);
component_main_text_draw(
ALIGN_LEFT, VALIGN_TOP,
"\n"
"\n"
"\n"
"\n"
" Region: %s\n"
" Unique ID: %.4s\n"
" Version: %hhu\n"
" Disk type: %d\n"
"\n"
" %s%s",
format_disk_region(menu->load.disk_info.region),
menu->load.disk_info.id,
menu->load.disk_info.version,
menu->load.disk_info.disk_type,
menu->load.rom_path ? "ROM: " : "",
menu->load.rom_path ? path_last_get(menu->load.rom_path) : ""
);
component_actions_bar_text_draw(
ALIGN_LEFT, VALIGN_TOP,
"A: Load and run 64DD disk\n"
"B: Exit"
);
if (menu->load.rom_path) {
component_actions_bar_text_draw(
ALIGN_RIGHT, VALIGN_TOP,
"R: Load with ROM"
);
}
}
rdpq_detach_show();
}
static void draw_progress (float progress) {
surface_t *d = (progress >= 1.0f) ? display_get() : display_try_get();
if (d) {
rdpq_attach(d, NULL);
component_background_draw();
component_loader_draw(progress);
rdpq_detach_show();
}
}
static void load (menu_t *menu) {
cart_load_err_t err;
if (menu->load.rom_path && load_rom) {
err = cart_load_n64_rom_and_save(menu, draw_progress);
if (err != CART_LOAD_OK) {
menu_show_error(menu, cart_load_convert_error_message(err));
return;
}
}
err = cart_load_64dd_ipl_and_disk(menu, draw_progress);
if (err != CART_LOAD_OK) {
menu_show_error(menu, cart_load_convert_error_message(err));
return;
}
menu->next_mode = MENU_MODE_BOOT;
menu->boot_params->device_type = load_rom ? BOOT_DEVICE_TYPE_ROM : BOOT_DEVICE_TYPE_64DD;
menu->boot_params->tv_type = BOOT_TV_TYPE_PASSTHROUGH;
menu->boot_params->detect_cic_seed = true;
}
void view_load_disk_init (menu_t *menu) {
if (menu->load.disk_path) {
path_free(menu->load.disk_path);
menu->load.disk_path = NULL;
}
load_pending = false;
menu->load.disk_path = path_clone_push(menu->browser.directory, menu->browser.entry->name);
disk_err_t err = disk_info_load(path_get(menu->load.disk_path), &menu->load.disk_info);
if (err != DISK_OK) {
menu_show_error(menu, convert_error_message(err));
}
}
void view_load_disk_display (menu_t *menu, surface_t *display) {
process(menu);
draw(menu, display);
if (load_pending) {
load_pending = false;
load(menu);
}
}

View File

@ -75,7 +75,7 @@ static void draw (menu_t *menu, surface_t *d) {
}
static void draw_progress (float progress) {
surface_t *d = display_try_get();
surface_t *d = (progress >= 1.0f) ? display_get() : display_try_get();
if (d) {
rdpq_attach(d, NULL);

View File

@ -1,140 +1,110 @@
#include "../cart_load.h"
#include "../rom_database.h"
#include "../rom_info.h"
#include "boot/boot.h"
#include "views.h"
static bool load_pending;
static rom_header_t rom_header;
static component_boxart_t *boxart;
static char *format_rom_endian (rom_endian_type_t endian) {
switch (endian) {
case ROM_BIG_ENDIAN:
case IPL_BIG_ENDIAN:
return "Big (default)";
case ROM_LITTLE_ENDIAN:
return "Little (unsupported)";
case ROM_MID_BIG_ENDIAN:
return "Byte swapped";
default:
return "Unknown";
static char *convert_error_message (disk_err_t err) {
switch (err) {
case ROM_ERR_IO: return "I/O error during loading ROM information";
case ROM_ERR_NO_FILE: return "Couldn't open ROM file";
default: return "Unknown ROM info load error";
}
}
static char *format_rom_media_type (rom_media_type_t media_type) {
static const char *format_rom_endianness (endianness_t endianness) {
switch (endianness) {
case ENDIANNESS_BIG: return "Big (default)";
case ENDIANNESS_LITTLE: return "Little (unsupported)";
case ENDIANNESS_BYTE_SWAP: return "Byte swapped";
default: return "Unknown";
}
}
static const char *format_rom_media_type (category_type_t media_type) {
switch (media_type) {
case N64_CART:
return "Cartridge";
case N64_DISK:
return "Disk";
case N64_CART_EXPANDABLE:
return "Cartridge (Expandable)";
case N64_DISK_EXPANDABLE:
return "Disk (Expandable)";
case N64_ALECK64:
return "Aleck64";
default:
return "Unknown";
case N64_CART: return "Cartridge";
case N64_DISK: return "Disk";
case N64_CART_EXPANDABLE: return "Cartridge (Expandable)";
case N64_DISK_EXPANDABLE: return "Disk (Expandable)";
case N64_ALECK64: return "Aleck64";
default: return "Unknown";
}
}
static char *format_rom_destination_market (rom_destination_market_t market_type) {
static const char *format_rom_destination_market (destination_type_t market_type) {
// TODO: These are all assumptions and should be corrected if required.
// From http://n64devkit.square7.ch/info/submission/pal/01-01.html
switch (market_type) {
case MARKET_JAPANESE_MULTI:
return "Japanese & English"; // 1080 Snowboarding JPN
case MARKET_BRAZILIAN:
return "Brazilian (Portuguese)";
case MARKET_CHINESE:
return "Chinese";
case MARKET_GERMAN:
return "German";
case MARKET_NORTH_AMERICA:
return "American English";
case MARKET_FRENCH:
return "French";
case MARKET_DUTCH:
return "Dutch";
case MARKET_ITALIAN:
return "Italian";
case MARKET_JAPANESE:
return "Japanese";
case MARKET_KOREAN:
return "Korean";
case MARKET_CANADIAN:
return "Canadaian (English & French)";
case MARKET_SPANISH:
return "Spanish";
case MARKET_AUSTRALIAN:
return "Australian (English)";
case MARKET_SCANDINAVIAN:
return "Scandinavian";
case MARKET_GATEWAY64_NTSC:
return "LodgeNet/Gateway (NTSC)";
case MARKET_GATEWAY64_PAL:
return "LodgeNet/Gateway (PAL)";
case MARKET_EUROPEAN_BASIC:
return "PAL (includes English)"; // Mostly EU but is used on some Australian ROMs
case MARKET_OTHER_X: // FIXME: AUS HSV Racing ROM's and Asia Top Gear Rally use this so not only EUR
return "Regional (non specific)";
case MARKET_OTHER_Y:
return "European (non specific)";
case MARKET_OTHER_Z:
return "Regional (unknown)";
default:
return "Unknown";
case MARKET_JAPANESE_MULTI: return "Japanese & English"; // 1080 Snowboarding JPN
case MARKET_BRAZILIAN: return "Brazilian (Portuguese)";
case MARKET_CHINESE: return "Chinese";
case MARKET_GERMAN: return "German";
case MARKET_NORTH_AMERICA: return "American English";
case MARKET_FRENCH: return "French";
case MARKET_DUTCH: return "Dutch";
case MARKET_ITALIAN: return "Italian";
case MARKET_JAPANESE: return "Japanese";
case MARKET_KOREAN: return "Korean";
case MARKET_CANADIAN: return "Canadaian (English & French)";
case MARKET_SPANISH: return "Spanish";
case MARKET_AUSTRALIAN: return "Australian (English)";
case MARKET_SCANDINAVIAN: return "Scandinavian";
case MARKET_GATEWAY64_NTSC: return "LodgeNet/Gateway (NTSC)";
case MARKET_GATEWAY64_PAL: return "LodgeNet/Gateway (PAL)";
case MARKET_EUROPEAN_BASIC: return "PAL (includes English)"; // Mostly EU but is used on some Australian ROMs
case MARKET_OTHER_X: return "Regional (non specific)"; // FIXME: AUS HSV Racing ROM's and Asia Top Gear Rally use this so not only EUR
case MARKET_OTHER_Y: return "European (non specific)";
case MARKET_OTHER_Z: return "Regional (unknown)";
default: return "Unknown";
}
}
static char *format_rom_save_type (db_savetype_t save_type) {
static const char *format_rom_save_type (save_type_t save_type) {
switch (save_type) {
case DB_SAVE_TYPE_NONE:
return "None";
case DB_SAVE_TYPE_EEPROM_4K:
return "EEPROM 4K";
case DB_SAVE_TYPE_EEPROM_16K:
return "EEPROM 16K";
case DB_SAVE_TYPE_SRAM:
return "SRAM";
case DB_SAVE_TYPE_SRAM_BANKED:
return "SRAM Banked";
case DB_SAVE_TYPE_SRAM_128K:
return "SRAM 128K";
case DB_SAVE_TYPE_FLASHRAM:
return "FlashRAM";
case DB_SAVE_TYPE_CPAK:
return "Controller Pak";
default:
return "Unknown";
case SAVE_TYPE_NONE: return "None";
case SAVE_TYPE_EEPROM_4K: return "EEPROM 4K";
case SAVE_TYPE_EEPROM_16K: return "EEPROM 16K";
case SAVE_TYPE_SRAM: return "SRAM";
case SAVE_TYPE_SRAM_BANKED: return "SRAM Banked";
case SAVE_TYPE_SRAM_128K: return "SRAM 128K";
case SAVE_TYPE_FLASHRAM: return "FlashRAM";
case SAVE_TYPE_FLASHRAM_PKST2: return "FlashRAM (Pokemon Stadium 2)";
default: return "Unknown";
}
}
static char *format_rom_expansion_pak_info (rom_memorytype_t expansion_pak_info) {
static char *format_rom_expansion_pak_info (expansion_pak_t expansion_pak_info) {
switch (expansion_pak_info) {
case DB_MEMORY_EXPANSION_REQUIRED:
return "Required";
case DB_MEMORY_EXPANSION_RECOMMENDED:
return "Recommended";
case DB_MEMORY_EXPANSION_SUGGESTED:
return "Suggested";
case DB_MEMORY_EXPANSION_FAULTY:
return "May require ROM patch";
default:
return "Not required";
case EXPANSION_PAK_REQUIRED: return "Required";
case EXPANSION_PAK_RECOMMENDED: return "Recommended";
case EXPANSION_PAK_SUGGESTED: return "Suggested";
case EXPANSION_PAK_FAULTY: return "May require ROM patch";
default: return "Not required";
}
}
static float format_rom_clockrate (uint32_t clockrate) {
/* Generally ROMs have a clock rate of 0x0000000F which signifies they used the default 62.5MHz clock. */
if (clockrate == 0x0F) {
return 62.5;
static const char *format_cic_type (cic_type_t cic_type) {
switch (cic_type) {
case CIC_5101: return "5101";
case CIC_5167: return "5167";
case CIC_6101: return "6101";
case CIC_7102: return "7102";
case CIC_6102_7101: return "6102 / 7101";
case CIC_x103: return "6103 / 7103";
case CIC_x105: return "6105 / 7105";
case CIC_x106: return "6106 / 7106";
case CIC_8301: return "8301";
case CIC_8302: return "8302";
case CIC_8303: return "8303";
case CIC_8401: return "8401";
case CIC_8501: return "8501";
default: return "Unknown";
}
/* If it did not, we need to show the different value. */
return (float) clockrate / 1000000;
}
@ -170,32 +140,34 @@ static void draw (menu_t *menu, surface_t *d) {
"\n"
"\n"
"\n"
" Endian: %s\n"
" Endianness: %s\n"
" Title: %.20s\n"
" Media type: %c - %s\n"
" Unique ID: %.2s\n"
" Destination market: %c - %s\n"
" Game code: %c%c%c%c\n"
" Media type: %s\n"
" Destination market: %s\n"
" Version: %hhu\n"
" Checksum: 0x%016llX\n"
" Check code: 0x%016llX\n"
" Save type: %s\n"
" Expansion PAK: %s\n"
"\n"
" Extra information:\n"
" CIC: %s\n"
" Boot address: 0x%08lX\n"
" SDK version: %.1f%c\n"
" Clock Rate: %.2fMHz\n",
format_rom_endian(rom_header.config_flags),
rom_header.title,
rom_header.metadata.media_type, format_rom_media_type(rom_header.metadata.media_type),
(char *) (&rom_header.metadata.unique_identifier),
rom_header.metadata.destination_market, format_rom_destination_market(rom_header.metadata.destination_market),
rom_header.metadata.version,
rom_header.checksum,
format_rom_save_type(rom_db_match_save_type(rom_header)),
format_rom_expansion_pak_info(rom_db_match_expansion_pak(rom_header)),
rom_header.boot_address,
(float) ((rom_header.sdk_version >> 8) & 0xFF) / 10.0f, (char) (rom_header.sdk_version & 0xFF),
format_rom_clockrate(rom_header.clock_rate)
format_rom_endianness(menu->load.rom_info.endianness),
menu->load.rom_info.title,
menu->load.rom_info.game_code[0], menu->load.rom_info.game_code[1], menu->load.rom_info.game_code[2], menu->load.rom_info.game_code[3],
format_rom_media_type(menu->load.rom_info.category_code),
format_rom_destination_market(menu->load.rom_info.destination_code),
menu->load.rom_info.version,
menu->load.rom_info.check_code,
format_rom_save_type(menu->load.rom_info.save_type),
format_rom_expansion_pak_info(menu->load.rom_info.features.expansion_pak),
format_cic_type(menu->load.rom_info.cic_type),
menu->load.rom_info.boot_address,
(menu->load.rom_info.libultra.version / 10.0f), menu->load.rom_info.libultra.revision,
menu->load.rom_info.clock_rate
);
component_actions_bar_text_draw(
@ -211,7 +183,7 @@ static void draw (menu_t *menu, surface_t *d) {
}
static void draw_progress (float progress) {
surface_t *d = display_try_get();
surface_t *d = (progress >= 1.0f) ? display_get() : display_try_get();
if (d) {
rdpq_attach(d, NULL);
@ -225,7 +197,7 @@ static void draw_progress (float progress) {
}
static void load (menu_t *menu) {
cart_load_err_t err = cart_load_n64_rom_and_save(menu, &rom_header, draw_progress);
cart_load_err_t err = cart_load_n64_rom_and_save(menu, draw_progress);
if (err != CART_LOAD_OK) {
menu_show_error(menu, cart_load_convert_error_message(err));
@ -244,15 +216,21 @@ static void deinit (void) {
void view_load_rom_init (menu_t *menu) {
if (menu->load.rom_path) {
path_free(menu->load.rom_path);
menu->load.rom_path = NULL;
}
load_pending = false;
path_t *path = path_clone_push(menu->browser.directory, menu->browser.entry->name);
menu->load.rom_path = path_clone_push(menu->browser.directory, menu->browser.entry->name);
rom_header = file_read_rom_header(path_get(path));
rom_err_t err = rom_info_load(path_get(menu->load.rom_path), &menu->load.rom_info);
if (err != ROM_OK) {
menu_show_error(menu, convert_error_message(err));
}
boxart = component_boxart_init(rom_header.metadata.media_type, rom_header.metadata.unique_identifier);
path_free(path);
boxart = component_boxart_init(menu->load.rom_info.game_code);
}
void view_load_rom_display (menu_t *menu, surface_t *display) {

View File

@ -4,6 +4,7 @@
#define SEEK_SECONDS (5)
#define SEEK_SECONDS_FAST (60)
static char *convert_error_message (mp3player_err_t err) {
@ -47,8 +48,8 @@ static void process (menu_t *menu) {
menu_show_error(menu, convert_error_message(err));
}
} else if (menu->actions.go_left || menu->actions.go_right) {
int seconds = (menu->actions.go_left ? -SEEK_SECONDS : SEEK_SECONDS);
err = mp3player_seek(seconds);
int seconds = menu->actions.go_fast ? SEEK_SECONDS_FAST : SEEK_SECONDS;
err = mp3player_seek(menu->actions.go_left ? (-seconds) : seconds);
if (err != MP3PLAYER_OK) {
menu_show_error(menu, convert_error_message(err));
}

View File

@ -2,10 +2,6 @@
#include "views.h"
static void process (menu_t *menu) {
menu->next_mode = MENU_MODE_BROWSER;
}
static void draw (menu_t *menu, surface_t *d) {
rdpq_attach_clear(d, NULL);
rdpq_detach_show();
@ -13,11 +9,9 @@ static void draw (menu_t *menu, surface_t *d) {
void view_startup_init (menu_t *menu) {
// Nothing to initialize (yet)
menu->next_mode = MENU_MODE_BROWSER;
}
void view_startup_display (menu_t *menu, surface_t *display) {
process(menu);
draw(menu, display);
}

View File

@ -3,21 +3,23 @@
#include "views.h"
static int controllers;
static int joypad[4];
static int accessory[4];
static char *format_accessory (int controller) {
switch (accessory[controller]) {
case ACCESSORY_RUMBLEPAK:
static char *format_accessory (int joypad) {
switch (accessory[joypad]) {
case JOYPAD_ACCESSORY_TYPE_RUMBLE_PAK:
return "[Rumble Pak is inserted]";
case ACCESSORY_MEMPAK:
case JOYPAD_ACCESSORY_TYPE_CONTROLLER_PAK:
return "[Controller Pak is inserted]";
case ACCESSORY_VRU:
return "[VRU is inserted]";
case ACCESSORY_TRANSFERPAK:
case JOYPAD_ACCESSORY_TYPE_TRANSFER_PAK:
return "[Transfer Pak is inserted]";
case ACCESSORY_NONE:
case JOYPAD_ACCESSORY_TYPE_BIO_SENSOR:
return "[BIO Sensor is inserted]";
case JOYPAD_ACCESSORY_TYPE_SNAP_STATION:
return "[Snap Station is inserted]";
case JOYPAD_ACCESSORY_TYPE_NONE:
return "";
default:
return "[unknown accessory inserted]";
@ -43,8 +45,6 @@ static void draw (menu_t *menu, surface_t *d) {
"N64 SYSTEM INFORMATION"
);
time_t current_time = time(NULL);
component_main_text_draw(
ALIGN_LEFT, VALIGN_TOP,
"\n"
@ -57,12 +57,12 @@ static void draw (menu_t *menu, surface_t *d) {
"JoyPad 2 is %sconnected %s\n"
"JoyPad 3 is %sconnected %s\n"
"JoyPad 4 is %sconnected %s\n",
current_time >= 0 ? ctime(&current_time) : "Unknown\n",
menu->current_time >= 0 ? ctime(&menu->current_time) : "Unknown\n",
is_memory_expanded() ? "" : "not ",
(controllers & CONTROLLER_1_INSERTED) ? "" : "not ", format_accessory(0),
(controllers & CONTROLLER_2_INSERTED) ? "" : "not ", format_accessory(1),
(controllers & CONTROLLER_3_INSERTED) ? "" : "not ", format_accessory(2),
(controllers & CONTROLLER_4_INSERTED) ? "" : "not ", format_accessory(3)
(joypad[0]) ? "" : "not ", format_accessory(0),
(joypad[1]) ? "" : "not ", format_accessory(1),
(joypad[2]) ? "" : "not ", format_accessory(2),
(joypad[3]) ? "" : "not ", format_accessory(3)
);
component_actions_bar_text_draw(
@ -76,10 +76,9 @@ static void draw (menu_t *menu, surface_t *d) {
void view_system_info_init (menu_t *menu) {
controllers = get_controllers_present();
for (int i = 0; i < 4; i++) {
accessory[i] = identify_accessory(i);
JOYPAD_PORT_FOREACH (port) {
joypad[port] = (joypad_get_style(port) != JOYPAD_STYLE_NONE);
accessory[port] = joypad_get_accessory_type(port);
}
}

View File

@ -41,6 +41,9 @@ void view_credits_display (menu_t *menu, surface_t *display);
void view_load_rom_init (menu_t *menu);
void view_load_rom_display (menu_t *menu, surface_t *display);
void view_load_disk_init (menu_t *menu);
void view_load_disk_display (menu_t *menu, surface_t *display);
void view_load_emulator_init (menu_t *menu);
void view_load_emulator_display (menu_t *menu, surface_t *display);