Merge branch 'develop' into cpak-management

This commit is contained in:
Robin Jones 2024-09-30 23:31:52 +01:00 committed by GitHub
commit 056e664941
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 231 additions and 123 deletions

View File

@ -1,6 +1,6 @@
FROM debian:bookworm-slim
ARG SC64_DEPLOYER_VERSION=v2.18.0
ARG SC64_DEPLOYER_VERSION=v2.20.0
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install build-essential doxygen git python3 wget -y && \

View File

@ -100,7 +100,6 @@ $(SPNG_OBJS): N64_CFLAGS+=-isystem $(SOURCE_DIR)/libs/miniz -DSPNG_USE_MINIZ -fc
$(FILESYSTEM_DIR)/FiraMonoBold.font64: MKFONT_FLAGS+=-c 1 --size 16 -r 20-7F -r 80-1FF -r 2026-2026 --ellipsis 2026,1
$(FILESYSTEM_DIR)/%.wav64: AUDIOCONV_FLAGS=--wav-compress 1
$(@info $(shell mkdir -p ./$(FILESYSTEM_DIR) &> /dev/null))
$(FILESYSTEM_DIR)/%.font64: $(ASSETS_DIR)/%.ttf

View File

@ -49,22 +49,45 @@ An open source menu for N64 flashcarts.
## Experimental features
These features are subject to change:
### ROM Boxart
To use boxart, place PNG files in the `/menu/boxart` folder on the SD card with the following dimensions:
* Standard covers: 158x112
* 64DD covers: 129x112
* Japanese covers: 112x158
Each file must be named according to the 2 letter ROM ID, or 3 letter ROM ID including media type.
i.e. for GoldenEye 2 letters, this would be `GE.png`.
i.e. for GoldenEye 3 letters, this would be `NGE.png`.
You can download these boxart packs:
### GamePak sprites
To use N64 `GamePak` sprites, place `PNG` files within the `sd:/menu/boxart/` folder.
[American Boxart](https://mega.nz/file/6cNGwSqI#8X5ukb65n3YMlGaUtSOGXkKo9HxVnnMOgqn94Epcr7w)
[European Boxart](https://mega.nz/file/O7AjDbRJ#VnVU10dq8HQvBUQptppI6PAcQMb8-Zembqav8WtAQ_M)
#### Supported sprites
These must be `PNG` files that use the following dimensions:
* Standard N64 GamePak boxart sprites: 158x112
* Japanese N64 GamePak boxart sprites: 112x158
* 64DD boxart sprites: 129x112
[64DD Boxart](https://mega.nz/file/O3JzwD7B#BYl1aV-pbrJ-MxWUbM_K0yGVIRbmSoxJJZqQInRzZyM)
They will be loaded by directories using each character of the full 4 character Game Code (as identified in the menus ROM information).
i.e. for GoldenEye NTSC USA (NGEE), this would be `sd:/menu/boxart/N/G/E/E/boxart_front.png`.
i.e. for GoldenEye PAL (NGEP), this would be `sd:/menu/boxart/N/G/E/P/boxart_front.png`.
To improve compatibility between regions (as a fallback), you may exclude the region ID (last matched directory) for GamePaks to match with 3 letter IDs instead:
i.e. for GoldenEye, this would be `sd:/menu/boxart/N/G/E/boxart_front.png`.
**Note1:** Excluding the region ID may show the wrong boxart.
**Note2:** For future support, boxart sprites should also include: `boxart_back.png`, `boxart_top.png`, `boxart_bottom.png`, `boxart_left.png`, `boxart_right.png`.
#### Compatibilty mode
If you cannot yet satisfy the correct boxart layout, The menu still has **deprecated** support for filenames containing the Game ID.
**Note:** This will add a noticeable delay for displaying parts of the menu.
Each file must be named according to the 2,3 or 4 letter GamePak ID (matched in this order).
i.e.
* for GoldenEye 4 letters, this would be `sd:/menu/boxart/NGEE.png` and/or `sd:/menu/boxart/NGEP.png`.
* for GoldenEye 3 letters, this would be `sd:/menu/boxart/NGE.png`.
* for GoldenEye 2 letters, this would be `sd:/menu/boxart/GE.png`.
As a starting point, here are some links to boxart packs:
* [Japan Boxart](https://mega.nz/file/KyJR0B6B#ERabLautAVPaqJTIdBSv4ghbudNhK7hnEr2ZS1Q6ub0)
* [American Boxart](https://mega.nz/file/rugAFYSQ#JHfgCU2amzNVpC4S6enP3vg--wtAAwsziKa7cej6QCc)
* [European Boxart](https://mega.nz/file/OmIV3aAK#kOWdutK1_41ffN64R6thbU7HEPR_M9qO0YM2mNG6RbQ)
* [64DD Boxart](https://mega.nz/file/ay5wQIxJ#k3PF-VMLrZJxJTr-BOaOKa2TBIK7c2t4zwbdshsQl40)
### Menu Settings

View File

@ -36,7 +36,7 @@ SD:\
│ │ ├── NDDJ2.n64
│ │ └── NDXJ0.n64
│ │
│ └── emulators
│ └── emulators\
│ ├── neon64bu.rom
│ ├── sodium64.z64
│ ├── gb.v64

View File

@ -8,11 +8,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.18.0/sc64-deployer-windows-v2.18.0.zip)
* Download the deployer [here](https://github.com/Polprzewodnikowy/SummerCart64/releases/download/v2.20.0/sc64-deployer-windows-v2.20.0.zip)
* Extract and place `sc64deployer.exe` in the `tools/sc64` directory.
Make sure that your firmware is compatible (currently v2.18.0+)
See: [here](https://github.com/Polprzewodnikowy/SummerCart64/blob/v2.18.0/docs/00_quick_startup_guide.md#firmware-backupupdate)
Make sure that your firmware is compatible (currently v2.20.0+)
See: [here](https://github.com/Polprzewodnikowy/SummerCart64/blob/v2.20.0/docs/00_quick_startup_guide.md#firmware-backupupdate)
##### From the devcontainer
It is not currently possible to directly communicate with USB devices.

@ -1 +1 @@
Subproject commit 9bae49994bf1c796f9939ea1aa7c133813b9053f
Subproject commit 9dd994151ae3f3709f1f80224e6b654aac8be6b4

View File

@ -75,6 +75,8 @@ static bool d64_has_feature (flashcart_features_t feature) {
case FLASHCART_FEATURE_64DD: return false;
case FLASHCART_FEATURE_RTC: return true;
case FLASHCART_FEATURE_USB: return true;
case FLASHCART_FEATURE_AUTO_CIC: return true;
case FLASHCART_FEATURE_AUTO_REGION: return true;
default: return false;
}
}

View File

@ -29,6 +29,8 @@ typedef enum {
FLASHCART_FEATURE_64DD,
FLASHCART_FEATURE_RTC,
FLASHCART_FEATURE_USB,
FLASHCART_FEATURE_AUTO_CIC,
FLASHCART_FEATURE_AUTO_REGION,
} flashcart_features_t;
/** @brief Flashcart save type enumeration */

View File

@ -254,6 +254,8 @@ static bool sc64_has_feature (flashcart_features_t feature) {
case FLASHCART_FEATURE_64DD: return true;
case FLASHCART_FEATURE_RTC: return true;
case FLASHCART_FEATURE_USB: return true;
case FLASHCART_FEATURE_AUTO_CIC: return true;
case FLASHCART_FEATURE_AUTO_REGION: return true;
default: return false;
}
}

2
src/libs/miniz vendored

@ -1 +1 @@
Subproject commit 16413c213de38e703d883006193734e8b1178d5d
Subproject commit 1ff82be7d67f5c2f8b5497f538eea247861e0717

View File

@ -25,8 +25,16 @@ static void actions_clear (menu_t *menu) {
}
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);
joypad_8way_t held_dir;
joypad_8way_t fast_dir;
JOYPAD_PORT_FOREACH (i) {
held_dir = joypad_get_direction(i, JOYPAD_2D_DPAD | JOYPAD_2D_STICK);
fast_dir = joypad_get_direction(i, JOYPAD_2D_C);
if (held_dir != JOYPAD_8WAY_NONE || fast_dir != JOYPAD_8WAY_NONE) {
break;
}
}
if (fast_dir != JOYPAD_8WAY_NONE) {
held_dir = fast_dir;
@ -82,7 +90,14 @@ static void actions_update_direction (menu_t *menu) {
}
static void actions_update_buttons (menu_t *menu) {
joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1);
joypad_buttons_t pressed;
JOYPAD_PORT_FOREACH (i) {
pressed = joypad_get_buttons_pressed(i);
if (pressed.raw) {
break;
}
}
if (pressed.a) {
menu->actions.enter = true;
@ -98,6 +113,12 @@ static void actions_update_buttons (menu_t *menu) {
}
void actions_init (void) {
JOYPAD_PORT_FOREACH (port) {
joypad_set_rumble_active(port, false);
}
}
void actions_update (menu_t *menu) {
joypad_poll();

View File

@ -11,6 +11,7 @@
#include "menu_state.h"
void actions_init (void);
void actions_update (menu_t *menu);

View File

@ -11,6 +11,41 @@
#include <libdragon.h>
#include "menu_state.h"
/** @brief File image Enumeration. */
typedef enum {
/** @brief Boxart image from the front */
IMAGE_BOXART_FRONT,
/** @brief Boxart image from the back */
IMAGE_BOXART_BACK,
/** @brief Boxart image from the top */
IMAGE_BOXART_TOP,
/** @brief Boxart image from the bottom */
IMAGE_BOXART_BOTTOM,
/** @brief Boxart image from the left side */
IMAGE_BOXART_LEFT,
/** @brief Boxart image from the right side */
IMAGE_BOXART_RIGHT,
/** @brief GamePak image from the front */
IMAGE_GAMEPAK_FRONT,
/** @brief GamePak image from the back */
IMAGE_GAMEPAK_BACK,
/** @brief File image thumbnail */
IMAGE_THUMBNAIL,
/** @brief List end marker */
IMAGE_TYPE_END
} file_image_type_t;
/**
* @addtogroup
@ -64,7 +99,7 @@ typedef struct {
surface_t *image;
} component_boxart_t;
component_boxart_t *component_boxart_init (const char *storage_prefix, char *game_code);
component_boxart_t *component_boxart_init (const char *storage_prefix, char *game_code, file_image_type_t current_image_view);
void component_boxart_free (component_boxart_t *b);
void component_boxart_draw (component_boxart_t *b);

View File

@ -98,9 +98,6 @@ static void prepare_background (component_background_t *c) {
return;
}
uint16_t image_center_x = (c->image->width / 2);
uint16_t image_center_y = (c->image->height / 2);
// Darken the image
rdpq_attach(c->image, NULL);
rdpq_mode_push();
@ -108,15 +105,13 @@ static void prepare_background (component_background_t *c) {
rdpq_set_prim_color(BACKGROUND_OVERLAY_COLOR);
rdpq_mode_combiner(RDPQ_COMBINER_FLAT);
rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY);
rdpq_fill_rectangle(
0 - (DISPLAY_CENTER_X - image_center_x),
0 - (DISPLAY_CENTER_Y - image_center_y),
DISPLAY_WIDTH - (DISPLAY_CENTER_X - image_center_x),
DISPLAY_HEIGHT - (DISPLAY_CENTER_Y - image_center_y)
);
rdpq_fill_rectangle(0, 0, c->image->width, c->image->height);
rdpq_mode_pop();
rdpq_detach();
uint16_t image_center_x = (c->image->width / 2);
uint16_t image_center_y = (c->image->height / 2);
// Prepare display list
rspq_block_begin();
rdpq_mode_push();

View File

@ -17,9 +17,9 @@ static void png_decoder_callback (png_err_t err, surface_t *decoded_image, void
}
component_boxart_t *component_boxart_init (const char *storage_prefix, char *game_code) {
component_boxart_t *component_boxart_init (const char *storage_prefix, char *game_code, file_image_type_t current_image_view) {
component_boxart_t *b;
char file_name[8];
char boxart_id_path[8];
if ((b = calloc(1, sizeof(component_boxart_t))) == NULL) {
return NULL;
@ -29,22 +29,84 @@ component_boxart_t *component_boxart_init (const char *storage_prefix, char *gam
path_t *path = path_init(storage_prefix, BOXART_DIRECTORY);
sprintf(file_name, "%.3s.png", game_code);
path_push(path, file_name);
if (png_decoder_start(path_get(path), BOXART_WIDTH_MAX, BOXART_HEIGHT_MAX, png_decoder_callback, b) == PNG_OK) {
path_free(path);
return b;
}
path_pop(path);
sprintf(boxart_id_path, "%c/%c/%c/%c", game_code[0], game_code[1], game_code[2], game_code[3]);
path_push(path, boxart_id_path);
// TODO: This is bad, we should only check for 3 letter codes
sprintf(file_name, "%.2s.png", game_code + 1);
path_push(path, file_name);
if (png_decoder_start(path_get(path), BOXART_WIDTH_MAX, BOXART_HEIGHT_MAX, png_decoder_callback, b) == PNG_OK) {
path_free(path);
return b;
if (!directory_exists(path_get(path))) { // Allow boxart to not specify the region code.
path_pop(path);
}
if (directory_exists(path_get(path))) {
switch (current_image_view) {
case IMAGE_GAMEPAK_FRONT:
path_push(path, "gamepak_front.png");
case IMAGE_GAMEPAK_BACK:
path_push(path, "gamepak_back.png");
case IMAGE_BOXART_BACK:
path_push(path, "boxart_back.png");
case IMAGE_BOXART_LEFT:
path_push(path, "boxart_left.png");
case IMAGE_BOXART_RIGHT:
path_push(path, "boxart_right.png");
case IMAGE_BOXART_BOTTOM:
path_push(path, "boxart_bottom.png");
case IMAGE_BOXART_TOP:
path_push(path, "boxart_top.png");
default:
path_push(path, "boxart_front.png");
}
if (file_exists(path_get(path))) {
if (png_decoder_start(path_get(path), BOXART_WIDTH_MAX, BOXART_HEIGHT_MAX, png_decoder_callback, b) == PNG_OK) {
path_free(path);
return b;
}
}
}
else { // compatibility mode
char file_name[8];
// reset the directory path used for boxart.
path = path_init(storage_prefix, BOXART_DIRECTORY);
sprintf(file_name, "%c%c%c%c.png", game_code[0], game_code[1], game_code[2], game_code[3]);
path_push(path, file_name);
if (file_exists(path_get(path))) {
if (png_decoder_start(path_get(path), BOXART_WIDTH_MAX, BOXART_HEIGHT_MAX, png_decoder_callback, b) == PNG_OK) {
path_free(path);
return b;
}
}
path_pop(path);
sprintf(file_name, "%c%c%c.png", game_code[0], game_code[1], game_code[2]);
path_push(path, file_name);
if (file_exists(path_get(path))) {
if (file_exists(path_get(path))) {
if (png_decoder_start(path_get(path), BOXART_WIDTH_MAX, BOXART_HEIGHT_MAX, png_decoder_callback, b) == PNG_OK) {
path_free(path);
return b;
}
}
}
else {
path_pop(path);
sprintf(file_name, "%c%c.png", game_code[1], game_code[2]);
path_push(path, file_name);
if (file_exists(path_get(path))) {
if (png_decoder_start(path_get(path), BOXART_WIDTH_MAX, BOXART_HEIGHT_MAX, png_decoder_callback, b) == PNG_OK) {
path_free(path);
return b;
}
}
}
}
// TODO: return default image.
path_free(path);
free(b);

View File

@ -27,53 +27,19 @@
#define MENU_CACHE_DIRECTORY "cache"
#define BACKGROUND_CACHE_FILE "background.data"
#define FRAMERATE_DIVIDER (2)
#define LAG_REPORT (false)
#define INTERLACED (true)
#define FPS_LIMIT (30.0f)
static menu_t *menu;
static tv_type_t tv_type;
static volatile int frame_counter = 0;
extern tv_type_t __boot_tvtype;
static void frame_counter_handler (void) {
frame_counter += 1;
}
static void frame_counter_reset (void) {
#if LAG_REPORT
static int accumulated = 0;
if (frame_counter > FRAMERATE_DIVIDER) {
accumulated += frame_counter - FRAMERATE_DIVIDER;
debugf(
"LAG: %d additional frame(s) displayed since last draw (accumulated: %d)\n",
frame_counter - FRAMERATE_DIVIDER,
accumulated
);
}
#endif
frame_counter = 0;
}
static void menu_init (boot_params_t *boot_params) {
joypad_init();
timer_init();
rtc_init();
rspq_init();
rdpq_init();
dfs_init(DFS_DEFAULT_LOCATION);
sound_init_default();
JOYPAD_PORT_FOREACH (port) {
joypad_set_rumble_active(port, false);
}
menu = calloc(1, sizeof(menu_t));
assert(menu != NULL);
menu->boot_params = boot_params;
menu->mode = MENU_MODE_NONE;
menu->next_mode = MENU_MODE_STARTUP;
@ -82,6 +48,19 @@ static void menu_init (boot_params_t *boot_params) {
menu->next_mode = MENU_MODE_FAULT;
}
joypad_init();
timer_init();
rtc_init();
rspq_init();
rdpq_init();
dfs_init(DFS_DEFAULT_LOCATION);
actions_init();
sound_init_default();
sound_init_sfx();
hdmi_clear_game_id();
path_t *path = path_init(menu->storage_prefix, MENU_DIRECTORY);
directory_create(path_get(path));
@ -91,6 +70,15 @@ static void menu_init (boot_params_t *boot_params) {
settings_load(&menu->settings);
path_pop(path);
resolution_t resolution = {
.width = 640,
.height = 480,
.interlaced = INTERLACED ? INTERLACE_HALF : INTERLACE_OFF,
.pal60 = menu->settings.pal60_enabled,
};
display_init(resolution, DEPTH_16_BPP, 2, GAMMA_NONE, INTERLACED ? FILTERS_DISABLED : FILTERS_RESAMPLE);
display_set_fps_limit(FPS_LIMIT);
path_push(path, MENU_CUSTOM_FONT_FILE);
fonts_init(path_get(path));
path_pop(path);
@ -103,40 +91,20 @@ static void menu_init (boot_params_t *boot_params) {
path_free(path);
menu->boot_params = boot_params;
sound_use_sfx(menu->settings.sound_enabled);
menu->browser.directory = path_init(menu->storage_prefix, menu->settings.default_directory);
if (!directory_exists(path_get(menu->browser.directory))) {
path_free(menu->browser.directory);
menu->browser.directory = path_init(menu->storage_prefix, "/");
}
hdmi_clear_game_id();
tv_type = get_tv_type();
if ((tv_type == TV_PAL) && menu->settings.pal60_enabled) {
// HACK: Set TV type to NTSC, so PAL console would output 60 Hz signal instead.
__boot_tvtype = TV_NTSC;
}
sound_init_sfx();
if (menu->settings.sound_enabled) {
sound_use_sfx(true);
}
display_init(RESOLUTION_640x480, DEPTH_16_BPP, 2, GAMMA_NONE, FILTERS_DISABLED);
register_VI_handler(frame_counter_handler);
}
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.
__boot_tvtype = tv_type;
hdmi_send_game_id(menu->boot_params);
component_background_free();
path_free(menu->load.disk_path);
path_free(menu->load.rom_path);
for (int i = 0; i < menu->browser.entries; i++) {
@ -146,9 +114,7 @@ static void menu_deinit (menu_t *menu) {
path_free(menu->browser.directory);
free(menu);
component_background_free();
flashcart_deinit();
display_close();
sound_deinit();
@ -158,7 +124,7 @@ static void menu_deinit (menu_t *menu) {
timer_close();
joypad_close();
display_close();
flashcart_deinit();
}
typedef const struct {
@ -201,11 +167,9 @@ void menu_run (boot_params_t *boot_params) {
menu_init(boot_params);
while (true) {
surface_t *display = (frame_counter >= FRAMERATE_DIVIDER) ? display_try_get() : NULL;
surface_t *display = display_try_get();
if (display != NULL) {
frame_counter_reset();
actions_update(menu);
view_t *view = menu_get_view(menu->mode);

View File

@ -186,6 +186,9 @@ static const match_t database[] = {
MATCH_ID_REGION("NDKJ", SAVE_TYPE_EEPROM_4KBIT, FEAT_NONE), // Dark Rift [Space Dynamites (J)]
MATCH_ID_REGION("NPDJ", SAVE_TYPE_EEPROM_16KBIT, FEAT_CPAK | FEAT_RPAK | FEAT_TPAK | FEAT_EXP_PAK_REQUIRED),// Perfect Dark (J)
MATCH_ID("NPD", SAVE_TYPE_EEPROM_16KBIT, FEAT_CPAK | FEAT_RPAK | FEAT_TPAK | FEAT_EXP_PAK_RECOMMENDED), // Perfect Dark
MATCH_ID_REGION("NSVE", SAVE_TYPE_EEPROM_4KBIT, FEAT_RPAK), // Space Station Silicon Valley
MATCH_ID("NSV", SAVE_TYPE_EEPROM_4KBIT, FEAT_RPAK | FEAT_EXP_PAK_BROKEN), // Space Station Silicon Valley
@ -305,7 +308,6 @@ static const match_t database[] = {
MATCH_ID("NMV", SAVE_TYPE_EEPROM_16KBIT, FEAT_RPAK), // Mario Party 3
MATCH_ID("NMX", SAVE_TYPE_EEPROM_16KBIT, FEAT_CPAK | FEAT_RPAK), // Excitebike 64
MATCH_ID("NNB", SAVE_TYPE_EEPROM_16KBIT, FEAT_CPAK | FEAT_RPAK), // Kobe Bryant in NBA Courtside
MATCH_ID("NPD", SAVE_TYPE_EEPROM_16KBIT, FEAT_CPAK | FEAT_RPAK | FEAT_TPAK | FEAT_EXP_PAK_RECOMMENDED), // Perfect Dark
MATCH_ID("NPP", SAVE_TYPE_EEPROM_16KBIT, FEAT_CPAK), // Parlor! Pro 64: Pachinko Jikki Simulation Game
MATCH_ID("NR7", SAVE_TYPE_EEPROM_16KBIT, FEAT_TPAK), // Robot Poncots 64: 7tsu no Umi no Caramel
MATCH_ID("NRZ", SAVE_TYPE_EEPROM_16KBIT, FEAT_RPAK), // Ridge Racer 64

View File

@ -7,7 +7,7 @@ static void draw (menu_t *menu, surface_t *d) {
rdpq_clear(RGBA32(0x7F, 0x00, 0x00, 0xFF));
const char *firmware_message = (
"Supported firmware versions:\n"
"Minimum supported firmware versions:\n"
"64drive: 2.05+\n"
"EverDrive-64: ???+\n"
"SummerCart64: 2.17.0+"

View File

@ -170,7 +170,7 @@ void view_load_disk_init (menu_t *menu) {
menu_show_error(menu, convert_error_message(err));
}
boxart = component_boxart_init(menu->storage_prefix, menu->load.disk_info.id);
boxart = component_boxart_init(menu->storage_prefix, menu->load.disk_info.id, IMAGE_BOXART_FRONT);
}
void view_load_disk_display (menu_t *menu, surface_t *display) {

View File

@ -355,7 +355,7 @@ void view_load_rom_init (menu_t *menu) {
return;
}
boxart = component_boxart_init(menu->storage_prefix, menu->load.rom_info.game_code);
boxart = component_boxart_init(menu->storage_prefix, menu->load.rom_info.game_code, IMAGE_BOXART_FRONT);
component_context_menu_init(&options_context_menu);
}