retro-go/launcher/main/applications.c
Alex Duchesne 627ba8451d rg_utils: Removed rg_bucket alignment
Implicitly align to word size seems more than good enough.
2025-11-07 22:02:06 -05:00

716 lines
21 KiB
C

#include <rg_system.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <errno.h>
#include "applications.h"
#include "bookmarks.h"
#include "gui.h"
#define CRC_CACHE_MAGIC 0x21112223
#define CRC_CACHE_MAX_ENTRIES 8192
static struct __attribute__((__packed__))
{
uint32_t magic;
uint32_t count;
struct {
uint32_t key;
uint32_t crc;
} entries[CRC_CACHE_MAX_ENTRIES];
} *crc_cache;
static bool crc_cache_dirty = true;
static retro_app_t *apps[24];
static int apps_count = 0;
static int scan_folder_cb(const rg_scandir_t *entry, void *arg)
{
retro_app_t *app = (retro_app_t *)arg;
uint8_t type = RETRO_TYPE_INVALID;
// Skip hidden files
if (entry->basename[0] == '.')
return RG_SCANDIR_SKIP;
if (entry->is_file)
{
if (rg_extension_match(entry->basename, app->extensions))
type = RETRO_TYPE_FILE;
}
else if (entry->is_dir)
{
RG_LOGI("Found subdirectory '%s'", entry->path);
type = RETRO_TYPE_FOLDER;
}
if (type == RETRO_TYPE_INVALID)
return RG_SCANDIR_CONTINUE;
if (app->files_count + 1 > app->files_capacity)
{
size_t new_capacity = (app->files_capacity * 1.5) + 1;
retro_file_t *new_buf = realloc(app->files, new_capacity * sizeof(retro_file_t));
if (!new_buf)
{
RG_LOGW("Ran out of memory, file scanning stopped at %d entries ...", app->files_count);
return RG_SCANDIR_STOP;
}
app->files = new_buf;
app->files_capacity = new_capacity;
}
char *name = rg_bucket_insert(app->filenames, entry->basename, strlen(entry->basename) + 1);
if (!name)
{
RG_LOGW("Ran out of memory for names, file scanning stopped at %d entries ...", app->files_count);
return RG_SCANDIR_STOP;
}
app->files[app->files_count++] = (retro_file_t) {
.name = name,
.folder = rg_unique_string(entry->dirname),
.checksum = 0,
.missing_cover = 0,
.saves = 0,
.type = type,
.app = (void*)app,
};
return RG_SCANDIR_CONTINUE;
}
static int scan_saves_cb(const rg_scandir_t *entry, void *arg)
{
if (entry->is_file && rg_extension_match(entry->basename, "sav"))
{
retro_app_t *app = (retro_app_t *)arg;
for (size_t i = 0; i < app->files_count; i++)
{
retro_file_t *file = &app->files[i];
// Saves are the rom name with possibly `.sav` or `-0.sav` appended.
if (strncmp(entry->basename, file->name, strlen(file->name)) == 0)
{
// FIXME: Check if folder matches first
file->saves++;
break;
}
}
}
return RG_SCANDIR_CONTINUE;
}
static void application_init(retro_app_t *app)
{
RG_LOGI("Initializing application '%s' (%s)", app->description, app->partition);
if (app->initialized)
return;
rg_storage_mkdir(app->paths.covers);
rg_storage_mkdir(app->paths.saves);
rg_storage_mkdir(app->paths.roms);
rg_storage_scandir(app->paths.roms, scan_folder_cb, app, RG_SCANDIR_RECURSIVE);
rg_storage_scandir(app->paths.saves, scan_saves_cb, app, RG_SCANDIR_RECURSIVE);
// rg_storage_scandir(app->paths.covers, scan_folder_cb3, app, RG_SCANDIR_RECURSIVE);
app->use_crc_covers = rg_storage_exists(strcat(app->paths.covers, "/0"));
app->paths.covers[strlen(app->paths.covers) - 2] = 0;
app->initialized = true;
}
static const char *get_file_path(retro_file_t *file)
{
static char buffer[RG_PATH_MAX + 1];
RG_ASSERT_ARG(file);
snprintf(buffer, RG_PATH_MAX, "%s/%s", file->folder, file->name);
return buffer;
}
static void application_start(retro_file_t *file, int load_state)
{
RG_ASSERT_ARG(file);
char *part = strdup(file->app->partition);
char *name = strdup(file->app->short_name);
char *path = strdup(get_file_path(file));
int flags = (gui.startup_mode ? RG_BOOT_ONCE : 0);
if (load_state != -1)
{
flags |= RG_BOOT_RESUME;
flags |= (load_state << 4) & RG_BOOT_SLOT_MASK;
}
bookmark_add(BOOK_TYPE_RECENT, file); // This could relocate *file, but we no longer need it
rg_system_switch_app(part, name, path, flags);
}
static uint32_t crc_read_file(retro_file_t *file, bool interactive)
{
uint8_t buffer[0x800];
uint32_t crc_tmp = 0;
bool done = false;
int count = -1;
FILE *fp;
if (file == NULL)
return 0;
if ((fp = fopen(get_file_path(file), "rb")))
{
fseek(fp, file->app->crc_offset, SEEK_SET);
while (count != 0)
{
// Give up on any button press to improve responsiveness
if (interactive && (gui.joystick = rg_input_read_gamepad()))
break;
count = fread(buffer, 1, sizeof(buffer), fp);
crc_tmp = rg_crc32(crc_tmp, buffer, count);
}
done = feof(fp);
fclose(fp);
}
return done ? crc_tmp : 0;
}
static void crc_cache_init(void)
{
crc_cache = calloc(1, sizeof(*crc_cache));
if (!crc_cache)
{
RG_LOGE("Failed to allocate crc_cache!");
return;
}
void *data_ptr = crc_cache;
size_t data_len = sizeof(*crc_cache);
rg_storage_read_file(RG_BASE_PATH_CACHE "/crc32.bin", &data_ptr, &data_len, RG_FILE_USER_BUFFER);
if (crc_cache->magic == CRC_CACHE_MAGIC && crc_cache->count <= CRC_CACHE_MAX_ENTRIES)
{
RG_LOGI("Loaded CRC cache (entries: %d)", (int)crc_cache->count);
crc_cache_dirty = false;
}
else
{
crc_cache->count = 0;
}
}
static void crc_cache_save(void)
{
if (!crc_cache || !crc_cache_dirty)
return;
RG_LOGI("Saving CRC cache...");
size_t data_len = RG_MIN(8 + (crc_cache->count * sizeof(crc_cache->entries[0])), sizeof(*crc_cache));
crc_cache_dirty = !rg_storage_write_file(RG_BASE_PATH_CACHE"/crc32.bin", crc_cache, data_len, 0);
}
static uint32_t crc_cache_calc_key(retro_file_t *file)
{
// return ((uint64_t)rg_crc32(0, (void *)file->name, strlen(file->name)) << 33 | file->size);
// This should be reasonably unique
const char *path = get_file_path(file);
return rg_crc32(0, (const uint8_t *)path, strlen(path));
}
static int crc_cache_find(retro_file_t *file)
{
if (!crc_cache)
return -1;
uint32_t key = crc_cache_calc_key(file);
for (int i = 0; i < crc_cache->count; i++)
{
if (crc_cache->entries[i].key == key)
return i;
}
return -1;
}
static uint32_t crc_cache_lookup(retro_file_t *file)
{
int index = crc_cache_find(file);
if (index != -1)
return crc_cache->entries[index].crc;
return 0;
}
static void crc_cache_update(retro_file_t *file)
{
if (!crc_cache)
return;
uint32_t key = crc_cache_calc_key(file);
int index = crc_cache_find(file);
if (index == -1)
{
if (crc_cache->count < CRC_CACHE_MAX_ENTRIES)
index = crc_cache->count++;
else
index = rand() % CRC_CACHE_MAX_ENTRIES;
RG_LOGI("Adding %08X => %08X to cache (new total: %d)",
(int)key, (int)file->checksum, (int)crc_cache->count);
}
else
{
RG_LOGI("Updating %08X => %08X to cache (total: %d)",
(int)key, (int)file->checksum, (int)crc_cache->count);
}
crc_cache->magic = CRC_CACHE_MAGIC;
crc_cache->entries[index].key = key;
crc_cache->entries[index].crc = file->checksum;
crc_cache_dirty = true;
// crc_cache_save();
}
void crc_cache_prebuild(void)
{
if (!crc_cache)
return;
for (int i = 0; i < apps_count; i++)
{
retro_app_t *app = apps[i];
if (!app->available)
continue;
if (!app->initialized)
application_init(app);
for (int j = 0; j < app->files_count; j++)
{
retro_file_t *file = &app->files[j];
rg_gui_draw_message(_("Scanning %s %d/%d"), app->short_name, j, app->files_count);
// Give up on any button press to improve responsiveness
if (rg_input_read_gamepad())
break;
if (file->checksum)
continue;
if ((file->checksum = crc_cache_lookup(file)))
continue;
if ((file->checksum = crc_read_file(file, true)))
crc_cache_update(file);
}
if (rg_input_read_gamepad())
break;
crc_cache_save();
gui_redraw();
}
crc_cache_save();
}
static void tab_refresh(tab_t *tab, const char *selected)
{
retro_app_t *app = (retro_app_t *)tab->arg;
memset(&tab->status, 0, sizeof(tab->status));
const char *basepath = rg_unique_string(app->paths.roms);
const char *folder = rg_unique_string(tab->navpath ?: basepath);
size_t items_count = 0;
char *ext = NULL;
if (folder == basepath)
tab->navpath = NULL;
if (app->files_count > 0)
{
gui_resize_list(tab, app->files_count);
for (size_t i = 0; i < app->files_count; i++)
{
retro_file_t *file = &app->files[i];
if (file->type == RETRO_TYPE_INVALID || !file->name)
continue;
if (file->folder != folder && strcmp(file->folder, folder) != 0)
continue;
if (file->type == RETRO_TYPE_FOLDER)
{
listbox_item_t *item = &tab->listbox.items[items_count++];
snprintf(item->text, sizeof(item->text), "[%.40s]", file->name);
item->group = 1;
item->arg = file;
}
else if (file->type == RETRO_TYPE_FILE)
{
listbox_item_t *item = &tab->listbox.items[items_count++];
snprintf(item->text, sizeof(item->text), "%s", file->name);
if ((ext = strrchr(item->text, '.')))
*ext = 0;
item->group = 2;
item->arg = file;
}
}
}
gui_resize_list(tab, items_count);
gui_sort_list(tab);
if (items_count == 0)
{
gui_resize_list(tab, 6);
sprintf(tab->listbox.items[0].text, _("Welcome to Retro-Go!"));
sprintf(tab->listbox.items[1].text, " ");
sprintf(tab->listbox.items[2].text, _("Place roms in folder: %s"), rg_relpath(app->paths.roms));
sprintf(tab->listbox.items[3].text, _("With file extension: %s"), app->extensions);
sprintf(tab->listbox.items[4].text, " ");
sprintf(tab->listbox.items[5].text, _("You can hide this tab in the menu"));
tab->listbox.cursor = 4;
}
else if (selected)
{
for (int i = 0; i < tab->listbox.length; i++)
{
retro_file_t *file = tab->listbox.items[i].arg;
if (file && strcmp(file->name, selected) == 0) // file->folder == selected->folder
{
tab->listbox.cursor = i;
break;
}
}
}
gui_scroll_list(tab, SCROLL_SET, tab->listbox.cursor);
}
static void event_handler(gui_event_t event, tab_t *tab)
{
listbox_item_t *item = gui_get_selected_item(tab);
retro_app_t *app = (retro_app_t *)tab->arg;
retro_file_t *file = (retro_file_t *)(item ? item->arg : NULL);
if (event == TAB_INIT)
{
application_init(app);
tab->navpath = NULL;
retro_file_t *selected = bookmark_find_by_app(BOOK_TYPE_RECENT, app);
if (selected) // && !rg_storage_exists(get_file_path(selected)))
{
// rg_storage_exists can take a long time on large folders (200ms), this is much faster
for (size_t i = 0; i < app->files_count; ++i)
{
retro_file_t *file = &app->files[i];
if (selected->folder == file->folder && strcmp(selected->name, file->name) == 0)
{
tab->navpath = file->folder;
break;
}
}
}
tab_refresh(tab, selected ? selected->name : NULL);
}
else if (event == TAB_DEINIT)
{
if (app && app->initialized)
{
rg_bucket_free(app->filenames);
app->filenames = rg_bucket_create(4096);
app->files_count = 0;
app->initialized = false;
}
}
else if (event == TAB_REFRESH)
{
tab_refresh(tab, NULL);
}
else if (event == TAB_ENTER || event == TAB_LEAVE || event == TAB_SCROLL)
{
gui_set_status(tab, NULL, "");
gui_set_preview(tab, NULL);
}
else if (event == TAB_IDLE)
{
if (file && !tab->preview && gui.idle_counter == 1)
gui_load_preview(tab);
}
else if (event == TAB_ACTION)
{
if (file)
{
if (file->type == RETRO_TYPE_FOLDER)
{
tab->navpath = rg_unique_string(get_file_path(file));
tab->listbox.cursor = 0;
tab_refresh(tab, NULL);
}
else if (file->type == RETRO_TYPE_FILE)
{
application_show_file_menu(file, false);
}
}
}
else if (event == TAB_BACK)
{
if (tab->navpath)
{
const char *from = rg_basename(rg_unique_string(tab->navpath));
tab->navpath = rg_unique_string(rg_dirname(tab->navpath));
tab->listbox.cursor = 0;
tab_refresh(tab, from);
}
}
}
bool application_path_to_file(const char *path, retro_file_t *file)
{
RG_ASSERT_ARG(path && file);
for (int i = 0; i < apps_count; ++i)
{
size_t baselen = strlen(apps[i]->paths.roms);
if (strncmp(path, apps[i]->paths.roms, baselen) == 0 && path[baselen] == '/')
{
*file = (retro_file_t) {
.name = strdup(rg_basename(path)),
.folder = rg_unique_string(rg_dirname(path)),
.saves = 0xFF, // We don't know, but we want gui_load_preview to check if needed
.type = RETRO_TYPE_FILE,
.app = apps[i],
};
return true;
}
}
return false;
}
bool application_get_file_crc32(retro_file_t *file)
{
uint32_t crc_tmp = 0;
if (file == NULL)
return false;
if (file->checksum > 0)
return true;
if ((crc_tmp = crc_cache_lookup(file)))
{
file->checksum = crc_tmp;
}
else
{
tab_t *tab = gui_get_current_tab();
gui_set_status(tab, NULL, "CRC32...");
gui_redraw(); // gui_draw_status(tab);
if ((crc_tmp = crc_read_file(file, true)))
{
file->checksum = crc_tmp;
crc_cache_update(file);
}
gui_set_status(tab, NULL, "");
gui_redraw(); // gui_draw_status(tab);
}
return file->checksum > 0;
}
static void show_file_info(retro_file_t *file)
{
char filesize[16];
char filecrc[16] = "Compute";
rg_stat_t info = rg_storage_stat(get_file_path(file));
if (!info.exists)
{
rg_gui_alert(_("File not found"), file->name);
return;
}
rg_gui_option_t options[] = {
{0, _("Name"), (char *)file->name, 1, NULL},
{0, _("Folder"), (char *)file->folder, 1, NULL},
{0, _("Size"), filesize, 1, NULL},
{3, _("CRC32"), filecrc, 1, NULL},
RG_DIALOG_SEPARATOR,
{5, _("Delete file"), NULL, 1, NULL},
{1, _("Close"), NULL, 1, NULL},
RG_DIALOG_END,
};
sprintf(filesize, "%d KB", (int)info.size / 1024);
while (true) // We loop in case we need to update the CRC
{
if (file->checksum)
sprintf(filecrc, "%08X (%d)", (int)file->checksum, file->app->crc_offset);
switch (rg_gui_dialog(_("File properties"), options, -1))
{
case 3:
application_get_file_crc32(file);
continue;
case 5:
if (rg_gui_confirm(_("Delete selected file?"), 0, 0))
{
if (remove(get_file_path(file)) == 0)
{
bookmark_remove(BOOK_TYPE_FAVORITE, file);
bookmark_remove(BOOK_TYPE_RECENT, file);
file->type = RETRO_TYPE_INVALID;
gui_event(TAB_REFRESH, gui_get_current_tab());
return;
}
}
continue;
default:
return;
}
}
}
void application_show_file_menu(retro_file_t *file, bool advanced)
{
char *rom_path = strdup(get_file_path(file));
char *sram_path = rg_emu_get_path(RG_PATH_SAVE_SRAM, rom_path);
rg_emu_states_t *savestates = rg_emu_get_states(rom_path, 4);
bool has_save = savestates->used > 0; // Don't rely on file->saves just yet
bool has_sram = rg_storage_exists(sram_path);
bool is_fav = bookmark_exists(BOOK_TYPE_FAVORITE, file);
int slot = -1;
rg_gui_option_t choices[] = {
{0, _("Resume game"), NULL, has_save, NULL},
{1, _("New game"), NULL, 1, NULL},
RG_DIALOG_SEPARATOR,
{3, is_fav ? _("Del favorite") : _("Add favorite"), NULL, 1, NULL},
{2, _("Delete save"), NULL, has_save || has_sram, NULL},
RG_DIALOG_SEPARATOR,
{4, _("Properties"), NULL, 1, NULL},
RG_DIALOG_END,
};
int sel = rg_gui_dialog(NULL, choices, has_save ? 0 : 1);
switch (sel)
{
case 0:
if ((slot = rg_gui_savestate_menu(_("Resume"), rom_path)) == -1)
break;
/* fallthrough */
case 1:
crc_cache_save();
gui_save_config();
application_start(file, slot);
break;
case 2:
while ((slot = rg_gui_savestate_menu(_("Delete save?"), rom_path)) != -1)
{
remove(savestates->slots[slot].preview);
remove(savestates->slots[slot].file);
// FIXME: We should update the last slot used here
}
if (has_sram && rg_gui_confirm(_("Delete sram file?"), 0, 0))
{
remove(sram_path);
}
break;
case 3:
if (is_fav)
bookmark_remove(BOOK_TYPE_FAVORITE, file);
else
bookmark_add(BOOK_TYPE_FAVORITE, file); // This could relocate *file
break;
case 4:
show_file_info(file);
break;
default:
break;
}
free(rom_path);
free(sram_path);
free(savestates);
// gui_redraw();
}
static void application(const char *desc, const char *name, const char *exts, const char *part, uint16_t crc_offset)
{
RG_ASSERT_ARG(desc && name && exts && part);
if (!rg_system_have_app(part))
{
RG_LOGI("Application '%s' (%s) not present, skipping", desc, part);
return;
}
retro_app_t *app = calloc(1, sizeof(retro_app_t));
apps[apps_count++] = app;
snprintf(app->description, sizeof(app->description), "%s", desc);
snprintf(app->short_name, sizeof(app->short_name), "%s", name);
snprintf(app->partition, sizeof(app->partition), "%s", part);
snprintf(app->extensions, sizeof(app->extensions), " %s ", exts);
snprintf(app->paths.covers, RG_PATH_MAX, RG_BASE_PATH_COVERS "/%s", app->short_name);
snprintf(app->paths.saves, RG_PATH_MAX, RG_BASE_PATH_SAVES "/%s", app->short_name);
snprintf(app->paths.roms, RG_PATH_MAX, RG_BASE_PATH_ROMS "/%s", app->short_name);
app->available = rg_system_have_app(app->partition);
app->files = calloc(100, sizeof(retro_file_t));
app->files_capacity = 100;
app->filenames = rg_bucket_create(4096);
app->crc_offset = crc_offset;
gui_add_tab(app->short_name, app->description, app, event_handler);
}
void applications_init(void)
{
application("Nintendo Entertainment System", "nes", "nes fc fds nsf zip", "retro-core", 16);
application("Super Nintendo", "snes", "smc sfc zip", "retro-core", 0);
application("Nintendo Gameboy", "gb", "gb gbc zip", "retro-core", 0);
application("Nintendo Gameboy Color", "gbc", "gbc gb zip", "retro-core", 0);
application("Nintendo Gameboy Advance", "gba", "gba zip", "gbsp", 0);
application("Nintendo Game & Watch", "gw", "gw", "retro-core", 0);
// application("Sega SG-1000", "sg1", "sms sg sg1", "retro-core", 0);
application("Sega Master System", "sms", "sms sg zip", "retro-core", 0);
application("Sega Game Gear", "gg", "gg zip", "retro-core", 0);
application("Sega Mega Drive", "md", "md gen bin zip", "gwenesis", 0);
application("Coleco ColecoVision", "col", "col rom zip", "retro-core", 0);
application("NEC PC Engine", "pce", "pce zip", "retro-core", 0);
application("Atari Lynx", "lnx", "lnx zip", "retro-core", 64);
// application("Atari 2600", "a26", "a26 zip", "stella-go", 0);
// application("Neo Geo Pocket Color", "ngp", "ngp ngc zip", "ngpocket-go", 0);
application("DOOM", "doom", "wad zip", "prboom-go", 0);
application("MSX", "msx", "rom mx1 mx2 dsk", "fmsx", 0);
// Special app to bootstrap native esp32 binaries from the SD card
// application("Bootstrap", "apps", "bin elf", "bootstrap", 0);
if (!rg_system_get_app()->lowMemoryMode)
crc_cache_init();
}