Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion PAKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@ Paks are platform specific. Inside the Emus and Tools folders you will find (or

Some platforms have multiple devices with unique qualities. NextUI differentiates these devices from the base platform with the `DEVICE` envar. eg. the "rg35xxplus" platform has two unique devices "cube" for the RG CubeXX, and "wide" for the RG34xx. It also supports "hdmi" for when outputting to HDMI. A pak can choose to use or ignore this envar.

# In-game pak shortcuts

A foreground tool such as a game guide can opt into the in-game pak picker by adding `in_game_shortcut` to its `pak.json`:

{
"name": "Game Guide",
"short_name": "GUIDE",
"type": "TOOL",
"in_game_shortcut": true
}

The user can then select it under Settings > In-Game > Pak Shortcut and open it with X from MinArch's root in-game menu. MinArch pauses game audio and keeps the core and game resident until the pak's `launch.sh` returns. The pak receives the current game path in `NEXTUI_ROM_PATH` and its emulator tag in `NEXTUI_EMU_TAG`.

The optional `short_name` is used for the X button hint and is limited to 12 characters. If omitted, the hint uses `name` instead.

Only foreground tools that exit cleanly should opt in. An in-game shortcut should not launch another emulator, leave background processes running, change display modes, or power off or reboot the device. Because MinArch remains resident, the pak should also avoid opening an audio device.

# The types of emulator pak

There are three basic types of emulator paks, which you chose depends on your goals and your desired level of NextUI integration.
Expand Down Expand Up @@ -104,4 +121,4 @@ But if a binary takes more than one second to initialize you might need to just

# Caveats

NextUI currently only supports the RGB565 pixel format and does not implement the OpenGL libretro APIs. It may be possible to use the stock firmware's retroarch instead of NextUI's minarch to run certain cores but that is left as an exercise for the reader.
NextUI currently only supports the RGB565 pixel format and does not implement the OpenGL libretro APIs. It may be possible to use the stock firmware's retroarch instead of NextUI's minarch to run certain cores but that is left as an exercise for the reader.
4 changes: 4 additions & 0 deletions skeleton/BASE/README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ TRIMUI BRICK / BRICK PRO / SMART PRO S

Buttons with no action of their own can be assigned a pak to launch: L3/R3 on the Brick, L4/R4 on the Brick Pro, and HOME on the Brick Pro and Smart Pro S. Assign them under Settings > Assignments. Assignments only apply in the main menu, not in-game. HOME no longer acts as a second menu button.

ALL

Compatible guide paks can be assigned under Settings > In-Game > Pak Shortcut. Open the in-game menu and press X to launch the assigned guide, then exit the guide to return to the paused game.

----------------------------------------
Quicksave & auto-resume

Expand Down
81 changes: 81 additions & 0 deletions workspace/all/common/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,47 @@
#include "utils.h"

NextUISettings settings = {0};
// CFG_init restores values through the public setters so validation stays in one
// place. Those setters normally persist immediately; suppress that while the
// settings file itself is being read or CFG_sync would truncate it mid-load.
static bool cfg_loading = false;

static void CFG_loadPakShortcutFile(void)
{
const char *shared_userdata = getenv("SHARED_USERDATA_PATH");
if (!shared_userdata || !shared_userdata[0])
return;

char path[MAX_PATH];
snprintf(path, sizeof(path), "%s/pakshortcut.txt", shared_userdata);
FILE *file = fopen(path, "r");
if (!file)
return;

char value[sizeof(settings.pakShortcut)];
if (fgets(value, sizeof(value), file))
{
value[strcspn(value, "\r\n")] = 0;
strncpy(settings.pakShortcut, value, sizeof(settings.pakShortcut) - 1);
settings.pakShortcut[sizeof(settings.pakShortcut) - 1] = '\0';
}
fclose(file);
}

static void CFG_syncPakShortcutFile(void)
{
const char *shared_userdata = getenv("SHARED_USERDATA_PATH");
if (!shared_userdata || !shared_userdata[0])
return;

char path[MAX_PATH];
snprintf(path, sizeof(path), "%s/pakshortcut.txt", shared_userdata);
FILE *file = fopen(path, "w");
if (!file)
return;
fprintf(file, "%s\n", settings.pakShortcut);
fclose(file);
}

// deprecated
uint32_t THEME_COLOR1_255;
Expand Down Expand Up @@ -66,6 +107,7 @@ void CFG_defaults(NextUISettings *cfg)
.muteLeds = CFG_DEFAULT_MUTELEDS,

.fnAction = {CFG_DEFAULT_FN_ACTION, CFG_DEFAULT_FN_ACTION, CFG_DEFAULT_FN_ACTION},
.pakShortcut = CFG_DEFAULT_PAK_SHORTCUT,

.screenTimeoutSecs = CFG_DEFAULT_SCREENTIMEOUTSECS,
.suspendTimeoutSecs = CFG_DEFAULT_SUSPENDTIMEOUTSECS,
Expand Down Expand Up @@ -141,6 +183,7 @@ static void setPaletteNameRaw(const char *name);

void CFG_init(FontLoad_callback_t cb, ColorSet_callback_t ccb)
{
cfg_loading = true;
CFG_defaults(&settings);
settings.onFontChange = cb;
settings.onColorSet = ccb;
Expand Down Expand Up @@ -366,6 +409,15 @@ void CFG_init(FontLoad_callback_t cb, ColorSet_callback_t ccb)
CFG_setFnAction(2, value);
continue;
}
if (strncmp(line, "pakShortcut=", 12) == 0)
{
char *value = line + 12;
value[strcspn(value, "\r\n")] = 0;
// Migrate the short-lived minuisettings.txt representation to
// its independent file, which older config writers cannot drop.
CFG_setPakShortcut(value);
continue;
}
if (sscanf(line, "artWidth=%i", &temp_value) == 1)
{
CFG_setGameArtWidth((double)temp_value / 100.0);
Expand Down Expand Up @@ -523,6 +575,13 @@ void CFG_init(FontLoad_callback_t cb, ColorSet_callback_t ccb)
fclose(file);
}

// This setting is intentionally independent of minuisettings.txt: every
// foreground utility historically rewrites that monolithic file, and an
// older utility would discard fields introduced by a newer build.
CFG_loadPakShortcutFile();

cfg_loading = false;

// load gfx related stuff until we drop the indirection
CFG_setColor(1, CFG_getColor(COLOR_MAIN));
CFG_setColor(2, CFG_getColor(COLOR_ACCENT));
Expand Down Expand Up @@ -948,6 +1007,20 @@ void CFG_setFnAction(int index, const char* action)
CFG_sync();
}

const char* CFG_getPakShortcut(void)
{
return settings.pakShortcut;
}

void CFG_setPakShortcut(const char* action)
{
if (!action)
action = "";
strncpy(settings.pakShortcut, action, sizeof(settings.pakShortcut) - 1);
settings.pakShortcut[sizeof(settings.pakShortcut) - 1] = '\0';
CFG_syncPakShortcutFile();
}

double CFG_getGameArtWidth(void)
{
return settings.gameArtWidth;
Expand Down Expand Up @@ -1453,6 +1526,10 @@ void CFG_get(const char *key, char *value)
{
sprintf(value, "%s", CFG_getFnAction(2));
}
else if (strcmp(key, "pakShortcut") == 0)
{
sprintf(value, "%s", CFG_getPakShortcut());
}
else if (strcmp(key, "artWidth") == 0)
{
sprintf(value, "%i", (int)(CFG_getGameArtWidth()) * 100);
Expand Down Expand Up @@ -1579,6 +1656,9 @@ void CFG_get(const char *key, char *value)

void CFG_sync(void)
{
if (cfg_loading)
return;

// write to file
char settingsPath[MAX_PATH];
const char *shared_userdata = getenv("SHARED_USERDATA_PATH");
Expand Down Expand Up @@ -1711,6 +1791,7 @@ void CFG_print(void)
printf("\t\"fn1action\": \"%s\",\n", settings.fnAction[0]);
printf("\t\"fn2action\": \"%s\",\n", settings.fnAction[1]);
printf("\t\"fn3action\": \"%s\",\n", settings.fnAction[2]);
printf("\t\"pakShortcut\": \"%s\",\n", settings.pakShortcut);
printf("\t\"artWidth\": %i,\n", (int)(settings.gameArtWidth * 100));
printf("\t\"wifi\": %i,\n", settings.wifi);
printf("\t\"defaultView\": %i,\n", settings.defaultView);
Expand Down
8 changes: 8 additions & 0 deletions workspace/all/common/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ typedef struct
// Action strings, "" == unassigned. See CFG_getFnAction().
char fnAction[FN_BUTTON_COUNT][256];

// Tools pak exposed on X in MinArch's root in-game menu.
char pakShortcut[256];

// Power
uint32_t screenTimeoutSecs;
uint32_t suspendTimeoutSecs;
Expand Down Expand Up @@ -237,6 +240,7 @@ typedef struct
#define CFG_DEFAULT_EXTRACTEDFILENAME false
#define CFG_DEFAULT_MUTELEDS false
#define CFG_DEFAULT_FN_ACTION "" // unassigned
#define CFG_DEFAULT_PAK_SHORTCUT "" // unassigned
#define CFG_DEFAULT_GAMEARTWIDTH 0.45
#define CFG_DEFAULT_WIFI false
#define CFG_DEFAULT_VIEW SCREEN_GAMELIST
Expand Down Expand Up @@ -393,6 +397,10 @@ void CFG_setMuteLEDs(bool);
// Returns "" for an out of range index.
const char* CFG_getFnAction(int index);
void CFG_setFnAction(int index, const char* action);
// The metadata-approved Tools pak exposed on X in MinArch's root in-game menu.
// Uses the same FN_ACTION_PAK_PREFIX representation as button assignments.
const char* CFG_getPakShortcut(void);
void CFG_setPakShortcut(const char* action);
// Set game art width percentage.
double CFG_getGameArtWidth(void);
void CFG_setGameArtWidth(double zeroToOne);
Expand Down
66 changes: 66 additions & 0 deletions workspace/all/common/utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,72 @@ int getInt(char* path) {
}
return i;
}

bool pakSupportsInGameShortcut(const char* pak_path) {
if (!pak_path || !pak_path[0]) return false;

char json_path[512];
if (snprintf(json_path, sizeof(json_path), "%s/pak.json", pak_path) >= (int)sizeof(json_path))
return false;

char* json = allocFile(json_path);
if (!json) return false;

const char* key = "\"in_game_shortcut\"";
char* value = strstr(json, key);
if (value) {
value += strlen(key);
while (isspace((unsigned char)*value)) value++;
if (*value == ':') value++;
else value = NULL;
}
if (value) {
while (isspace((unsigned char)*value)) value++;
}

bool supported = value && strncmp(value, "true", 4) == 0 &&
!isalnum((unsigned char)value[4]) && value[4] != '_';
free(json);
return supported;
}

static bool getPakJsonString(const char* json, const char* key, char* value, size_t value_size) {
char quoted_key[64];
if (!json || !value || value_size == 0 ||
snprintf(quoted_key, sizeof(quoted_key), "\"%s\"", key) >= (int)sizeof(quoted_key))
return false;

const char* start = strstr(json, quoted_key);
if (!start) return false;
start += strlen(quoted_key);
while (isspace((unsigned char)*start)) start++;
if (*start++ != ':') return false;
while (isspace((unsigned char)*start)) start++;
if (*start++ != '"') return false;

size_t length = 0;
while (*start && *start != '"' && length < PAK_SHORT_NAME_MAX && length + 1 < value_size)
value[length++] = *start++;
value[length] = '\0';
return length > 0;
}

bool getPakShortcutName(const char* pak_path, char* name, size_t name_size) {
if (!pak_path || !pak_path[0] || !name || name_size == 0) return false;
name[0] = '\0';

char json_path[512];
if (snprintf(json_path, sizeof(json_path), "%s/pak.json", pak_path) >= (int)sizeof(json_path))
return false;

char* json = allocFile(json_path);
if (!json) return false;

bool found = getPakJsonString(json, "short_name", name, name_size) ||
getPakJsonString(json, "name", name, name_size);
free(json);
return found;
}
void putInt(char* path, int value) {
char buffer[8];
sprintf(buffer, "%d", value);
Expand Down
6 changes: 6 additions & 0 deletions workspace/all/common/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
#include <stdint.h>
#include <stdbool.h>

#define PAK_SHORT_NAME_MAX 12


int prefixMatch(char* pre, const char* str);
int suffixMatch(char* suf,const char* str);
int exactMatch(const char* str1, const char* str2);
Expand Down Expand Up @@ -42,6 +45,9 @@ void getFile(char* path, char* buffer, size_t buffer_size);
void putInt(char* path, int value);
int getInt(char* path);

bool pakSupportsInGameShortcut(const char* pak_path);
bool getPakShortcutName(const char* pak_path, char* name, size_t name_size);

uint64_t getMicroseconds(void);

int clamp(int x, int lower, int upper);
Expand Down
Loading