diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 896d34cf..ccf500d3 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -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" diff --git a/Makefile b/Makefile index be770325..b8a84b61 100644 --- a/Makefile +++ b/Makefile @@ -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 \ diff --git a/README.md b/README.md index 22784228..a5cfc418 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/libdragon b/libdragon index 35267992..dd2202a6 160000 --- a/libdragon +++ b/libdragon @@ -1 +1 @@ -Subproject commit 352679922bad5403f8ea4b9123dfe0403d030bc2 +Subproject commit dd2202a6082cd3d92f4c22e64b27f148612c4c3a diff --git a/src/boot/boot.c b/src/boot/boot.c index b4ba7ce4..b4b066ee 100644 --- a/src/boot/boot.c +++ b/src/boot/boot.c @@ -1,8 +1,8 @@ #include -#include "boot.h" #include "boot_io.h" -#include "crc32.h" +#include "boot.h" +#include "cic.h" #define C0_STATUS_FR (1 << 26) @@ -14,71 +14,34 @@ 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; - } - } - - return false; + params->cic_seed = cic_get_seed(cic_detect(ipl3)); } -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) { params->tv_type = OS_INFO->tv_type; } 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 diff --git a/src/boot/boot.h b/src/boot/boot.h index d60b5725..61ff6546 100644 --- a/src/boot/boot.h +++ b/src/boot/boot.h @@ -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 */ diff --git a/src/boot/cic.c b/src/boot/cic.c new file mode 100644 index 00000000..6b74d217 --- /dev/null +++ b/src/boot/cic.c @@ -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; + } +} diff --git a/src/boot/cic.h b/src/boot/cic.h new file mode 100644 index 00000000..4f97c6c4 --- /dev/null +++ b/src/boot/cic.h @@ -0,0 +1,33 @@ +#ifndef CIC_H__ +#define CIC_H__ + + +#include + + +#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 diff --git a/src/boot/reboot.S b/src/boot/reboot.S index ec6dc9f2..e64a46f1 100644 --- a/src/boot/reboot.S +++ b/src/boot/reboot.S @@ -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 diff --git a/src/flashcart/64drive/64drive.c b/src/flashcart/64drive/64drive.c index 82a0e1ea..6bcee6cb 100644 --- a/src/flashcart/64drive/64drive.c +++ b/src/flashcart/64drive/64drive.c @@ -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, }; diff --git a/src/flashcart/flashcart.c b/src/flashcart/flashcart.c index d7938356..aa4ca54a 100644 --- a/src/flashcart/flashcart.c +++ b/src/flashcart/flashcart.c @@ -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,59 @@ 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 + 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 +127,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 +170,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 +190,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); +} diff --git a/src/flashcart/flashcart.h b/src/flashcart/flashcart.h index 844ab3cb..3a22c8ac 100644 --- a/src/flashcart/flashcart.h +++ b/src/flashcart/flashcart.h @@ -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 diff --git a/src/flashcart/sc64/sc64.c b/src/flashcart/sc64/sc64.c index 1d7245d4..a0878c9d 100644 --- a/src/flashcart/sc64/sc64.c +++ b/src/flashcart/sc64/sc64.c @@ -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, }; diff --git a/src/flashcart/sc64/sc64_ll.c b/src/flashcart/sc64/sc64_ll.c index fcf261f3..4d6f659e 100644 --- a/src/flashcart/sc64/sc64_ll.c +++ b/src/flashcart/sc64/sc64_ll.c @@ -1,5 +1,6 @@ #include +#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 diff --git a/src/flashcart/sc64/sc64_ll.h b/src/flashcart/sc64/sc64_ll.h index 1a0976b8..38e159e3 100644 --- a/src/flashcart/sc64/sc64_ll.h +++ b/src/flashcart/sc64/sc64_ll.h @@ -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); diff --git a/src/hdmi/hdmi.c b/src/hdmi/hdmi.c deleted file mode 100644 index aa6971c1..00000000 --- a/src/hdmi/hdmi.c +++ /dev/null @@ -1,61 +0,0 @@ -// Implementation based on https://gitlab.com/pixelfx-public/n64-game-id - -#include - -#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 - ); -} diff --git a/src/hdmi/hdmi.h b/src/hdmi/hdmi.h deleted file mode 100644 index 222974c8..00000000 --- a/src/hdmi/hdmi.h +++ /dev/null @@ -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 diff --git a/src/main.c b/src/main.c index f8dde253..d188c650 100644 --- a/src/main.c +++ b/src/main.c @@ -1,19 +1,14 @@ #include #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); diff --git a/src/menu/actions.c b/src/menu/actions.c index 98f4bd3b..f517a11f 100644 --- a/src/menu/actions.c +++ b/src/menu/actions.c @@ -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) { - 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) { + switch (final_dir) { + case JOYPAD_8WAY_NONE: + break; + case JOYPAD_8WAY_RIGHT: menu->actions.go_right = true; - } + 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); +} diff --git a/src/menu/cart_load.c b/src/menu/cart_load.c index 62f4bc73..2986d9a2 100644 --- a/src/menu/cart_load.c +++ b/src/menu/cart_load.c @@ -1,75 +1,97 @@ +#include + +#include + #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); diff --git a/src/menu/cart_load.h b/src/menu/cart_load.h index e5359ddd..c3fa7d7a 100644 --- a/src/menu/cart_load.h +++ b/src/menu/cart_load.h @@ -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); diff --git a/src/menu/components.h b/src/menu/components.h index 10a8437d..3a03a1d2 100644 --- a/src/menu/components.h +++ b/src/menu/components.h @@ -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); diff --git a/src/menu/components/boxart.c b/src/menu/components/boxart.c index 6419c1cf..90b23ccd 100644 --- a/src/menu/components/boxart.c +++ b/src/menu/components/boxart.c @@ -17,22 +17,21 @@ 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) { - free(b); - b = NULL; + 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; + } } } diff --git a/src/menu/components/common.c b/src/menu/components/common.c index 0001d063..5440cb08 100644 --- a/src/menu/components/common.c +++ b/src/menu/components/common.c @@ -122,8 +122,8 @@ void component_messagebox_draw (char *fmt, ...) { }, FNT_DEFAULT, formatted, ¶graph_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); diff --git a/src/menu/components/context_menu.c b/src/menu/components/context_menu.c index 205e1386..10663509 100644 --- a/src/menu/components/context_menu.c +++ b/src/menu/components/context_menu.c @@ -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, diff --git a/src/menu/components/file_list.c b/src/menu/components/file_list.c index 5da8d1e9..15a438fc 100644 --- a/src/menu/components/file_list.c +++ b/src/menu/components/file_list.c @@ -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( diff --git a/src/menu/disk_info.c b/src/menu/disk_info.c new file mode 100644 index 00000000..32680a3b --- /dev/null +++ b/src/menu/disk_info.c @@ -0,0 +1,212 @@ +#include +#include +#include +#include + +#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; +} diff --git a/src/menu/disk_info.h b/src/menu/disk_info.h new file mode 100644 index 00000000..12b7ab4b --- /dev/null +++ b/src/menu/disk_info.h @@ -0,0 +1,52 @@ +/** + * @file disk_info.h + * @brief 64DD disk information + * @ingroup menu + */ + +#ifndef DISK_INFO_H__ +#define DISK_INFO_H__ + + +#include +#include + + +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 diff --git a/src/menu/hdmi.c b/src/menu/hdmi.c new file mode 100644 index 00000000..41e30427 --- /dev/null +++ b/src/menu/hdmi.c @@ -0,0 +1,34 @@ +#include + +#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); +} diff --git a/src/menu/hdmi.h b/src/menu/hdmi.h new file mode 100644 index 00000000..ce59b5a4 --- /dev/null +++ b/src/menu/hdmi.h @@ -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 diff --git a/src/menu/menu.c b/src/menu/menu.c index 3a25e98f..a573bcdb 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -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 diff --git a/src/menu/menu_state.h b/src/menu/menu_state.h index 503d5dec..1c51b946 100644 --- a/src/menu/menu_state.h +++ b/src/menu/menu_state.h @@ -9,9 +9,12 @@ #include + #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; diff --git a/src/menu/rom_database.c b/src/menu/rom_database.c deleted file mode 100644 index 5ef5cb41..00000000 --- a/src/menu/rom_database.c +++ /dev/null @@ -1,278 +0,0 @@ -#include -#include "rom_database.h" -#include -#include -#include -#include - -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; -} diff --git a/src/menu/rom_database.h b/src/menu/rom_database.h deleted file mode 100644 index f04e8a4a..00000000 --- a/src/menu/rom_database.h +++ /dev/null @@ -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 - - -/** - * @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 diff --git a/src/menu/rom_info.c b/src/menu/rom_info.c new file mode 100644 index 00000000..5e3e084d --- /dev/null +++ b/src/menu/rom_info.c @@ -0,0 +1,738 @@ +#include + +#include + +#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; +} diff --git a/src/menu/rom_info.h b/src/menu/rom_info.h new file mode 100644 index 00000000..a2322ff7 --- /dev/null +++ b/src/menu/rom_info.h @@ -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 +#include + +#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 diff --git a/src/menu/settings.c b/src/menu/settings.c index a4a05f97..df642c61 100644 --- a/src/menu/settings.c +++ b/src/menu/settings.c @@ -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); diff --git a/src/menu/settings.h b/src/menu/settings.h index 39781a15..6206788c 100644 --- a/src/menu/settings.h +++ b/src/menu/settings.h @@ -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; diff --git a/src/menu/sound.c b/src/menu/sound.c index 39a1c5b4..d613b4f2 100644 --- a/src/menu/sound.c +++ b/src/menu/sound.c @@ -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; - } -} diff --git a/src/menu/sound.h b/src/menu/sound.h index 784dfca0..44ecf6bc 100644 --- a/src/menu/sound.h +++ b/src/menu/sound.h @@ -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 diff --git a/src/menu/views/browser.c b/src/menu/views/browser.c index 7ed8d0b6..2f7a6acf 100644 --- a/src/menu/views/browser.c +++ b/src/menu/views/browser.c @@ -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; diff --git a/src/menu/views/error.c b/src/menu/views/error.c index 120e40cd..8e565a55 100644 --- a/src/menu/views/error.c +++ b/src/menu/views/error.c @@ -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) { diff --git a/src/menu/views/fault.c b/src/menu/views/fault.c index e60b19f7..7128f666 100644 --- a/src/menu/views/fault.c +++ b/src/menu/views/fault.c @@ -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(); diff --git a/src/menu/views/load_disk.c b/src/menu/views/load_disk.c new file mode 100644 index 00000000..14d9d80b --- /dev/null +++ b/src/menu/views/load_disk.c @@ -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); + } +} diff --git a/src/menu/views/load_emulator.c b/src/menu/views/load_emulator.c index 777bfa63..b791aa02 100644 --- a/src/menu/views/load_emulator.c +++ b/src/menu/views/load_emulator.c @@ -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); diff --git a/src/menu/views/load_rom.c b/src/menu/views/load_rom.c index 55d2c8b3..9c5c4dca 100644 --- a/src/menu/views/load_rom.c +++ b/src/menu/views/load_rom.c @@ -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) { diff --git a/src/menu/views/music_player.c b/src/menu/views/music_player.c index 4d059626..7b3d9751 100644 --- a/src/menu/views/music_player.c +++ b/src/menu/views/music_player.c @@ -3,7 +3,8 @@ #include "views.h" -#define SEEK_SECONDS (5) +#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)); } diff --git a/src/menu/views/startup.c b/src/menu/views/startup.c index 7fb3837e..118d20dc 100644 --- a/src/menu/views/startup.c +++ b/src/menu/views/startup.c @@ -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); } diff --git a/src/menu/views/system_info.c b/src/menu/views/system_info.c index e60f1466..a0ebfcad 100644 --- a/src/menu/views/system_info.c +++ b/src/menu/views/system_info.c @@ -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(¤t_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( @@ -75,11 +75,10 @@ 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); +void view_system_info_init (menu_t *menu) { + JOYPAD_PORT_FOREACH (port) { + joypad[port] = (joypad_get_style(port) != JOYPAD_STYLE_NONE); + accessory[port] = joypad_get_accessory_type(port); } } diff --git a/src/menu/views/views.h b/src/menu/views/views.h index 186000d5..990474dd 100644 --- a/src/menu/views/views.h +++ b/src/menu/views/views.h @@ -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);