OreBolt-OS/modules/orebolt-retro/retro.c

601 lines
22 KiB
C
Executable File

/*
* retro.c -- OreBolt OS Module: Retro Game Launcher
*
* SPDX-License-Identifier: GPL-2.0-or-later
*
* Scans /data/roms/ for ROM files, auto-detects the target system by
* file extension, displays an LVGL picker menu, and launches the
* matching emulator binary via fork()+execv(). Standalone .mod
* binary with its own main() and LVGL init.
*
* Supported systems (by file extension):
* .nes - Nintendo Entertainment System
* .fds - Famicom Disk System
* .smc,.sfc,.fig - Super Nintendo (SNES)
* .gb - Game Boy
* .gbc - Game Boy Color
* .gba - Game Boy Advance
* .smd,.md,.bin,.gen - Sega Genesis / Mega Drive
* .sms - Sega Master System
* .gg - Sega Game Gear
* .pce - PC Engine / TurboGrafx-16
* .a26 - Atari 2600
* .ngp,.ngc - Neo Geo Pocket / Color
*
* Emulator binaries must be installed at the paths listed in
* rom_types[] below.
*
* INPUT MAPPING:
* The companion retro_input_mapper daemon (started by S98retro-input
* at boot) creates a virtual gamepad via uinput. Emulators should
* read from this virtual gamepad rather than /dev/input/event0.
*
* D-pad: Rotary CW/CCW with axis toggle (rotary press switches
* between vertical U/D and horizontal L/R modes, auto-
* reverts to V-mode after 3s idle).
* A: PLAY short press (confirm/jump/shoot)
* B: BACK short press (cancel/run)
* START: PLAY long press (1.5s)
* SELECT: BACK long press (1.5s)
* L/R: Side buttons PREV(165) / NEXT(163)
* X/Y: Combo buttons (PLAY+rotate / BACK+rotate)
*
* N64 is NOT supported -- the X1000E MIPS32r2 @ ~1 GHz with 64 MB RAM
* cannot run N64 emulation at playable framerates.
*
* Toolchain: mipsel-linux-musl-gcc (MIPS32r2, musl libc)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/input.h>
#include <signal.h>
#include <sys/wait.h>
#include <ctype.h>
#include "h2_ui.h"
#include "log_manager.h"
/* ------------------------------------------------------------------ */
/* ROM type database */
/* ------------------------------------------------------------------ */
#define MAX_ROM_TYPES 32
#define MAX_ROMS 256
#define ROM_DIR "/data/roms"
#define PATH_BUF 512
typedef struct {
const char *ext; /* file extension, lowercase, with dot */
const char *system; /* human-readable system name */
const char *emulator; /* absolute path to emulator binary */
const char *core; /* emulator-specific core/flag, or NULL */
} rom_type_t;
static const rom_type_t rom_db[MAX_ROM_TYPES] = {
/* --- Nintendo --- */
{ ".nes", "NES", "/usr/bin/fceux", NULL },
{ ".fds", "FDS", "/usr/bin/fceux", NULL },
{ ".smc", "SNES", "/usr/bin/snes9x", NULL },
{ ".sfc", "SNES", "/usr/bin/snes9x", NULL },
{ ".fig", "SNES", "/usr/bin/snes9x", NULL },
{ ".gb", "GameBoy", "/usr/bin/gambatte", NULL },
{ ".gbc", "GBC", "/usr/bin/gambatte", NULL },
{ ".gba", "GBA", "/usr/bin/gpsp", NULL },
/* --- Sega --- */
{ ".smd", "Genesis", "/usr/bin/dgen", NULL },
{ ".md", "Genesis", "/usr/bin/dgen", NULL },
{ ".gen", "Genesis", "/usr/bin/dgen", NULL },
{ ".bin", "Genesis", "/usr/bin/dgen", "-g" },
{ ".sms", "SMS", "/usr/bin/mednafen", "-ss" },
{ ".gg", "GameGear", "/usr/bin/mednafen", "-gg" },
/* --- NEC / Hudson --- */
{ ".pce", "PCE", "/usr/bin/mednafen", "-pce" },
/* --- Atari --- */
{ ".a26", "Atari2600", "/usr/bin/stella", NULL },
/* --- SNK --- */
{ ".ngp", "NeoGeoP", "/usr/bin/mednafen", "-ngp" },
{ ".ngc", "NeoGeoP", "/usr/bin/mednafen", "-ngp" },
/* sentinel */
{ NULL, NULL, NULL, NULL }
};
/* ------------------------------------------------------------------ */
/* ROM entry (populated at scan time) */
/* ------------------------------------------------------------------ */
typedef struct {
char path[PATH_BUF]; /* full path to ROM file */
char name[64]; /* display name (filename only) */
const rom_type_t *type; /* pointer into rom_db */
} rom_entry_t;
static rom_entry_t roms[MAX_ROMS];
static int rom_count = 0;
/* ------------------------------------------------------------------ */
/* Utility: lowercase the last N chars of a string in-place */
/* ------------------------------------------------------------------ */
static void str_lower_tail(char *s, int n) {
int len = (int)strlen(s);
int start = len - n;
if (start < 0) start = 0;
for (int i = start; s[i]; i++)
s[i] = (char)tolower((unsigned char)s[i]);
}
/* ------------------------------------------------------------------ */
/* Utility: extract filename from full path */
/* ------------------------------------------------------------------ */
static void extract_filename(const char *path, char *out, int outlen) {
const char *slash = strrchr(path, '/');
if (slash) slash++;
else slash = path;
/* strip the extension for display */
const char *dot = strrchr(slash, '.');
int namelen = dot ? (int)(dot - slash) : (int)strlen(slash);
if (namelen > outlen - 1) namelen = outlen - 1;
memcpy(out, slash, (size_t)namelen);
out[namelen] = '\0';
}
/* ------------------------------------------------------------------ */
/* Find the rom_type_t that matches a filename extension */
/* ------------------------------------------------------------------ */
static const rom_type_t *match_rom_type(const char *filename) {
char lower[PATH_BUF];
snprintf(lower, sizeof(lower), "%s", filename);
str_lower_tail(lower, 6); /* extensions are max 4 chars */
const char *dot = strrchr(lower, '.');
if (!dot) return NULL;
for (int i = 0; i < MAX_ROM_TYPES && rom_db[i].ext; i++) {
if (strcmp(dot, rom_db[i].ext) == 0)
return &rom_db[i];
}
return NULL;
}
/* ------------------------------------------------------------------ */
/* Scan ROM_DIR recursively and populate roms[] */
/* ------------------------------------------------------------------ */
static void scan_roms_recursive(const char *basepath) {
DIR *dir = opendir(basepath);
if (!dir) return;
struct dirent *de;
while ((de = readdir(dir)) != NULL) {
if (de->d_name[0] == '.') continue; /* skip hidden / . / .. */
char fullpath[PATH_BUF];
snprintf(fullpath, sizeof(fullpath), "%s/%s", basepath, de->d_name);
struct stat st;
if (lstat(fullpath, &st) != 0) continue;
if (S_ISDIR(st.st_mode)) {
/* recurse into subdirectories (system-named folders) */
scan_roms_recursive(fullpath);
continue;
}
if (!S_ISREG(st.st_mode)) continue;
const rom_type_t *t = match_rom_type(de->d_name);
if (!t) continue; /* unknown extension, skip */
if (rom_count >= MAX_ROMS) break;
rom_entry_t *r = &roms[rom_count];
snprintf(r->path, PATH_BUF, "%s", fullpath);
extract_filename(de->d_name, r->name, (int)sizeof(r->name));
r->type = t;
rom_count++;
}
closedir(dir);
}
/* ------------------------------------------------------------------ */
/* System filter state */
/* ------------------------------------------------------------------ */
static const char *system_filters[] = {
"ALL",
"NES", "FDS", "SNES",
"GameBoy", "GBC", "GBA",
"Genesis", "SMS", "GameGear",
"PCE", "Atari2600", "NeoGeoP",
NULL /* sentinel */
};
#define NUM_FILTERS (sizeof(system_filters) / sizeof(system_filters[0]) - 1)
static int current_filter = 0; /* index into system_filters, 0 = ALL */
/* ------------------------------------------------------------------ */
/* LVGL UI objects (module-global for event loop access) */
/* ------------------------------------------------------------------ */
static lv_obj_t *title_label = NULL;
static lv_obj_t *filter_label = NULL;
static lv_obj_t *count_label = NULL;
static lv_obj_t *rom_list = NULL;
static lv_obj_t *list_btns[MAX_ROMS];
static int visible_count = 0;
static int scroll_index = 0;
/* ------------------------------------------------------------------ */
/* Build the ROM list UI (called on filter change) */
/* ------------------------------------------------------------------ */
static void build_rom_list(void) {
/* clear existing list buttons */
if (rom_list) {
lv_obj_clean(rom_list);
}
visible_count = 0;
scroll_index = 0;
const char *filter = system_filters[current_filter];
int is_all = (current_filter == 0);
for (int i = 0; i < rom_count && visible_count < MAX_ROMS; i++) {
if (!is_all && strcmp(roms[i].type->system, filter) != 0)
continue;
/* build display text: "[SYSTEM] name" */
char display[80];
snprintf(display, sizeof(display), "%s %s",
roms[i].type->system, roms[i].name);
list_btns[visible_count] = lv_list_add_btn(rom_list,
LV_SYMBOL_PLAY,
display);
lv_obj_set_style_text_color(list_btns[visible_count],
COLOR_TEXT, LV_PART_MAIN);
lv_obj_set_style_text_font(list_btns[visible_count],
&lv_font_montserrat_12, LV_PART_MAIN);
visible_count++;
}
/* update count label */
char count_buf[48];
snprintf(count_buf, sizeof(count_buf),
"%d / %d ROMs", visible_count, rom_count);
lv_label_set_text(count_label, count_buf);
}
/* ------------------------------------------------------------------ */
/* Scroll the list */
/* ------------------------------------------------------------------ */
static void scroll_rom_list(int delta) {
scroll_index += delta;
if (scroll_index < 0) scroll_index = 0;
if (scroll_index >= visible_count) scroll_index = visible_count - 1;
if (visible_count > 0 && scroll_index < visible_count) {
lv_obj_scroll_to_view(list_btns[scroll_index], LV_ANIM_ON);
}
}
/* ------------------------------------------------------------------ */
/* Cycle system filter (rapid-rotate = cycle filter) */
/* ------------------------------------------------------------------ */
static void cycle_filter(void) {
current_filter++;
if (system_filters[current_filter] == NULL)
current_filter = 0;
char fbuf[48];
snprintf(fbuf, sizeof(fbuf), "Filter: %s", system_filters[current_filter]);
lv_label_set_text(filter_label, fbuf);
build_rom_list();
LOG_INF("filter changed to: %s", system_filters[current_filter]);
}
/* ------------------------------------------------------------------ */
/* Launch the selected ROM */
/* ------------------------------------------------------------------ */
static void launch_rom(int index) {
if (index < 0 || index >= visible_count) return;
/* find the actual rom_entry_t (skip filtered-out entries) */
const char *filter = system_filters[current_filter];
int is_all = (current_filter == 0);
int real_index = -1;
int target = 0;
for (int i = 0; i < rom_count; i++) {
if (!is_all && strcmp(roms[i].type->system, filter) != 0)
continue;
if (target == index) { real_index = i; break; }
target++;
}
if (real_index < 0) return;
const rom_entry_t *rom = &roms[real_index];
const rom_type_t *type = rom->type;
LOG_INF("launching ROM: %s [%s]", rom->name, type->system);
/* clear screen before launch */
lv_obj_clean(lv_scr_act());
lv_obj_t *launch_lbl = lv_label_create(lv_scr_act());
char launch_buf[80];
snprintf(launch_buf, sizeof(launch_buf),
"Launching %s...\n%s\n[BACK] to exit",
type->system, rom->name);
lv_label_set_text(launch_lbl, launch_buf);
lv_obj_set_style_text_color(launch_lbl, COLOR_ACCENT, LV_PART_MAIN);
lv_obj_align(launch_lbl, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_style_text_align(launch_lbl, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
/* flush the "Launching..." screen */
for (int i = 0; i < 10; i++) lv_timer_handler();
pid_t pid = fork();
if (pid == 0) {
/*
* Child: exec the emulator with the ROM path.
* If the emulator has a core flag (e.g. mednafen -ss), pass it.
*/
if (type->core) {
char *args[] = { (char *)type->emulator,
(char *)type->core,
(char *)rom->path,
NULL };
execv(type->emulator, args);
} else {
char *args[] = { (char *)type->emulator,
(char *)rom->path,
NULL };
execv(type->emulator, args);
}
/* exec failed -- emulator binary not found */
_exit(127);
}
if (pid < 0) {
LOG_ERR("fork failed for emulator launch");
return; /* fork failed */
}
LOG_INF("emulator child pid %d started", (int)pid);
/*
* Parent: non-blocking wait in event loop.
* BACK key sends SIGTERM to the emulator child.
* Rebuilds the ROM list when child exits.
*/
int input_fd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK);
struct input_event ev;
int child_running = 1;
while (child_running) {
lv_timer_handler();
/* non-blocking child check */
int status;
pid_t w = waitpid(pid, &status, WNOHANG);
if (w == pid) {
child_running = 0;
LOG_INF("emulator child %d exited", (int)pid);
break;
}
/* read input */
if (input_fd >= 0 &&
read(input_fd, &ev, sizeof(struct input_event)) > 0) {
if (ev.type == EV_KEY && ev.value == 1) {
if (ev.code == H2_KEY_BACK) {
kill(pid, SIGTERM);
/* give emulator 300ms to clean up */
usleep(300000);
waitpid(pid, &status, WNOHANG);
child_running = 0;
LOG_INF("emulator child %d terminated by user", (int)pid);
}
}
}
usleep(15000);
}
if (input_fd >= 0) close(input_fd);
/* rebuild the UI for the ROM picker */
lv_obj_clean(lv_scr_act());
}
/* ------------------------------------------------------------------ */
/* main -- module entry point */
/* ------------------------------------------------------------------ */
int main(void) {
init_h2_graphics_runtime("RETRO GAME LAUNCHER");
LOG_INF("retro module started");
lv_obj_t *scr = lv_scr_act();
/* ---- header ---- */
title_label = lv_label_create(scr);
lv_label_set_text(title_label, "RETRO GAME LAUNCHER");
lv_obj_align(title_label, LV_ALIGN_TOP_MID, 0, 8);
lv_obj_set_style_text_color(title_label, COLOR_PRIMARY, LV_PART_MAIN);
/* ---- filter indicator (PLAY = launch, rapid-rotate = filter) ---- */
filter_label = lv_label_create(scr);
lv_label_set_text(filter_label, "Filter: ALL");
lv_obj_align(filter_label, LV_ALIGN_TOP_LEFT, 8, 30);
lv_obj_set_style_text_color(filter_label, COLOR_ACCENT, LV_PART_MAIN);
lv_obj_set_style_text_font(filter_label, &lv_font_montserrat_10,
LV_PART_MAIN);
/* ---- ROM count ---- */
count_label = lv_label_create(scr);
lv_obj_align(count_label, LV_ALIGN_TOP_RIGHT, -8, 30);
lv_obj_set_style_text_color(count_label, lv_color_make(120, 120, 140),
LV_PART_MAIN);
lv_obj_set_style_text_font(count_label, &lv_font_montserrat_10,
LV_PART_MAIN);
/* ---- ROM list ---- */
rom_list = lv_list_create(scr);
lv_obj_set_size(rom_list, 296, 168);
lv_obj_align(rom_list, LV_ALIGN_TOP_MID, 0, 46);
lv_obj_set_style_bg_color(rom_list,
lv_color_make(18, 22, 32), LV_PART_MAIN);
lv_obj_set_style_border_width(rom_list, 1, LV_PART_MAIN);
lv_obj_set_style_border_color(rom_list,
lv_color_make(50, 55, 70), LV_PART_MAIN);
/* ---- status bar ---- */
lv_obj_t *status = lv_label_create(scr);
lv_label_set_text(status, "ROTATE=scroll PLAY=launch BACK=exit");
lv_obj_align(status, LV_ALIGN_BOTTOM_MID, 0, -4);
lv_obj_set_style_text_color(status, lv_color_make(80, 85, 100),
LV_PART_MAIN);
lv_obj_set_style_text_font(status, &lv_font_montserrat_10, LV_PART_MAIN);
/* ---- scan ROMs ---- */
scan_roms_recursive(ROM_DIR);
LOG_INF("ROM scan complete: %d ROM(s) found", rom_count);
build_rom_list();
if (rom_count == 0) {
lv_obj_t *empty = lv_label_create(rom_list);
lv_label_set_text(empty,
LV_SYMBOL_WARNING " No ROMs found.\n\n"
"Place ROM files in:\n"
" /data/roms/\n\n"
"Organize by folder:\n"
" /data/roms/nes/\n"
" /data/roms/snes/\n"
" /data/roms/gb/\n"
" ...etc");
lv_obj_set_style_text_color(empty, lv_color_make(160, 100, 60),
LV_PART_MAIN);
lv_obj_set_style_text_align(empty, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
}
/* ---- input loop ---- */
int input_fd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK);
struct input_event ev;
/* rotary accumulator for filter cycling:
* 8 fast clicks in < 200ms each = cycle filter
* (avoids needing a separate button for filter) */
int rotary_clicks = 0;
long long last_click_ms = 0;
while (1) {
lv_timer_handler();
if (input_fd >= 0 &&
read(input_fd, &ev, sizeof(struct input_event)) > 0) {
if (ev.type == EV_REL && ev.code == 0) {
if (ev.value != 0) {
/* track rapid scrolling for filter toggle */
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
long long now = (long long)ts.tv_sec * 1000 +
ts.tv_nsec / 1000000;
if (now - last_click_ms < 200) {
rotary_clicks++;
if (rotary_clicks >= 8) {
cycle_filter();
rotary_clicks = 0;
}
} else {
rotary_clicks = 1;
}
last_click_ms = now;
scroll_rom_list(ev.value > 0 ? 1 : -1);
}
}
if (ev.type == EV_KEY && ev.value == 1) {
if (ev.code == H2_KEY_PLAY && visible_count > 0) {
/* PLAY: launch selected ROM */
launch_rom(scroll_index);
/* after launch returns, rebuild UI */
lv_obj_clean(lv_scr_act());
/* re-create widgets */
title_label = lv_label_create(scr);
lv_label_set_text(title_label, "RETRO GAME LAUNCHER");
lv_obj_align(title_label, LV_ALIGN_TOP_MID, 0, 8);
lv_obj_set_style_text_color(title_label, COLOR_PRIMARY,
LV_PART_MAIN);
filter_label = lv_label_create(scr);
char fbuf[48];
snprintf(fbuf, sizeof(fbuf), "Filter: %s",
system_filters[current_filter]);
lv_label_set_text(filter_label, fbuf);
lv_obj_align(filter_label, LV_ALIGN_TOP_LEFT, 8, 30);
lv_obj_set_style_text_color(filter_label, COLOR_ACCENT,
LV_PART_MAIN);
lv_obj_set_style_text_font(filter_label,
&lv_font_montserrat_10, LV_PART_MAIN);
count_label = lv_label_create(scr);
lv_obj_align(count_label, LV_ALIGN_TOP_RIGHT, -8, 30);
lv_obj_set_style_text_color(count_label,
lv_color_make(120, 120, 140), LV_PART_MAIN);
lv_obj_set_style_text_font(count_label,
&lv_font_montserrat_10, LV_PART_MAIN);
rom_list = lv_list_create(scr);
lv_obj_set_size(rom_list, 296, 168);
lv_obj_align(rom_list, LV_ALIGN_TOP_MID, 0, 46);
lv_obj_set_style_bg_color(rom_list,
lv_color_make(18, 22, 32), LV_PART_MAIN);
lv_obj_set_style_border_width(rom_list, 1, LV_PART_MAIN);
lv_obj_set_style_border_color(rom_list,
lv_color_make(50, 55, 70), LV_PART_MAIN);
status = lv_label_create(scr);
lv_label_set_text(status,
"ROTATE=scroll PLAY=launch BACK=exit");
lv_obj_align(status, LV_ALIGN_BOTTOM_MID, 0, -4);
lv_obj_set_style_text_color(status,
lv_color_make(80, 85, 100), LV_PART_MAIN);
lv_obj_set_style_text_font(status,
&lv_font_montserrat_10, LV_PART_MAIN);
build_rom_list();
}
if (ev.code == H2_KEY_BACK) {
/* BACK: exit module */
break;
}
}
}
usleep(15000);
}
if (input_fd >= 0) close(input_fd);
LOG_INF("retro module exited");
return 0;
}