Skip to content
Merged
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
3 changes: 2 additions & 1 deletion runtime/include/psx_keybinds.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ typedef struct {

/* Initialize from <exe_dir>/keybinds.ini. Generates a default file if one does
* not exist. exe_path may be NULL/argv[0]/the exe directory itself — the .ini is
* placed in that file's directory (or the path itself when it is a directory). */
* placed in that file's directory (or the path itself when it is an existing
* directory or ends in a path separator). Executable names need no extension. */
void psx_keybinds_init(const char *exe_path);

/* Read-only view of the current bindings. */
Expand Down
22 changes: 16 additions & 6 deletions runtime/src/psx_keybinds.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <string.h>
#include <ctype.h>
#include <stddef.h>
#include <sys/stat.h>

/* Mouse-button pseudo-scancodes. Values sit above SDL's keyboard scancode
* space (SDL_NUM_SCANCODES == 512), so they flow through the existing
Expand Down Expand Up @@ -186,29 +187,38 @@ static const char *scancode_to_name(SDL_Scancode sc) {

static char s_ini_path[1024] = {0};

static int path_is_directory(const char *path) {
#ifdef _WIN32
struct _stat64 info;
return _stat64(path, &info) == 0 &&
(info.st_mode & _S_IFMT) == _S_IFDIR;
#else
struct stat info;
return stat(path, &info) == 0 && S_ISDIR(info.st_mode);
#endif
}

/* Resolve keybinds.ini alongside exe_path. exe_path may be a file (argv[0] /
* the exe) or a directory (the exe dir the launcher passes) — either works. */
static void derive_ini_path(const char *exe_path) {
if (!exe_path || !*exe_path) {
strcpy(s_ini_path, "keybinds.ini");
return;
}
/* Find the last path separator; if none, treat the whole thing as a dir. */
/* Find the executable's parent without guessing from its extension. */
const char *slash = NULL;
for (const char *p = exe_path; *p; p++)
if (*p == '/' || *p == '\\') slash = p;

/* Heuristic: a path ending in a separator, or with no extension in the last
* component, is treated as a directory; otherwise strip the file name. */
/* Explicit directory paths and existing directories are accepted. Linux
* executable names need no dot, while directory names may contain dots. */
size_t len = strlen(exe_path);
int ends_sep = (exe_path[len-1] == '/' || exe_path[len-1] == '\\');
const char *last = slash ? slash + 1 : exe_path;
int has_ext = strchr(last, '.') != NULL;

char dir[1024];
if (ends_sep) {
snprintf(dir, sizeof(dir), "%s", exe_path);
} else if (!has_ext) {
} else if (path_is_directory(exe_path)) {
/* directory path without trailing sep */
snprintf(dir, sizeof(dir), "%s/", exe_path);
} else if (slash) {
Expand Down
51 changes: 43 additions & 8 deletions runtime/tests/test_keyboard_pad_chord.c
Original file line number Diff line number Diff line change
Expand Up @@ -86,21 +86,56 @@ int main(int argc, char **argv) {
* loader permits shared sources, so construct this fixture through it. */
{
#ifdef _WIN32
if (_mkdir("keyboard_pad_chord_fixture") != 0) return 1;
if (_mkdir("keyboard_pad_chord_fixture.v1") != 0) return 1;
#else
if (mkdir("keyboard_pad_chord_fixture", 0755) != 0) return 1;
if (mkdir("keyboard_pad_chord_fixture.v1", 0755) != 0) return 1;
#endif
FILE *config = fopen("keyboard_pad_chord_fixture/keybinds.ini", "wb");
/* A real extensionless executable is a file, including on Windows.
* Its first launch must write beside it, then load the same settings. */
FILE *exe = fopen("keyboard_pad_chord_fixture.v1/fixture", "wb");
if (!exe) return 1;
fclose(exe);
psx_keybinds_init("keyboard_pad_chord_fixture.v1/fixture");
FILE *defaults = fopen("keyboard_pad_chord_fixture.v1/keybinds.ini", "rb");
expect_word("extensionless executable creates adjacent INI", 1,
defaults != NULL);
if (defaults) fclose(defaults);

FILE *config = fopen("keyboard_pad_chord_fixture.v1/keybinds.ini", "wb");
if (!config) return 1;
fputs("[player1]\ncross = Escape\ncircle = S, Escape\n"
fputs("[player1]\nup = W\nr1 = Q\ncross = Escape\ncircle = S, Escape\n"
"[player2]\ncross = Escape\n", config);
fclose(config);
psx_keybinds_init("keyboard_pad_chord_fixture/fixture.exe");
remove("keyboard_pad_chord_fixture/keybinds.ini");
psx_keybinds_init("keyboard_pad_chord_fixture.v1/fixture");
keys[SDL_SCANCODE_W] = 1;
expect_word("extensionless executable loads W as Up", 0xFFEFu,
psx_keybinds_pad_word(keys, 1));
keys[SDL_SCANCODE_W] = 0;

const char *paths[] = {
"keyboard_pad_chord_fixture.v1/fixture.exe",
"keyboard_pad_chord_fixture.v1",
"keyboard_pad_chord_fixture.v1/"
};
for (unsigned i = 0; i < sizeof(paths) / sizeof(paths[0]); ++i) {
psx_keybinds_reset_player(1);
psx_keybinds_init(paths[i]);
expect_scancode(paths[i], SDL_SCANCODE_W,
psx_keybinds_get_button(1, PSX_KB_UP));
}
psx_keybinds_init("keyboard_pad_chord_fixture.v1/fixture");
psx_keybinds_set_button(1, PSX_KB_UP, SDL_SCANCODE_V);
psx_keybinds_save();
psx_keybinds_reset_player(1);
psx_keybinds_init("keyboard_pad_chord_fixture.v1/fixture");
expect_scancode("extensionless executable saves and reloads", SDL_SCANCODE_V,
psx_keybinds_get_button(1, PSX_KB_UP));
remove("keyboard_pad_chord_fixture.v1/fixture");
remove("keyboard_pad_chord_fixture.v1/keybinds.ini");
#ifdef _WIN32
_rmdir("keyboard_pad_chord_fixture");
_rmdir("keyboard_pad_chord_fixture.v1");
#else
rmdir("keyboard_pad_chord_fixture");
rmdir("keyboard_pad_chord_fixture.v1");
#endif
expect_scancode("shared INI primary Escape", SDL_SCANCODE_ESCAPE,
psx_keybinds_get_button(1, PSX_KB_CROSS));
Expand Down
Loading