diff --git a/runtime/include/psx_keybinds.h b/runtime/include/psx_keybinds.h index dd3139fd7..dc8fdd89f 100644 --- a/runtime/include/psx_keybinds.h +++ b/runtime/include/psx_keybinds.h @@ -68,7 +68,8 @@ typedef struct { /* Initialize from /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. */ diff --git a/runtime/src/psx_keybinds.c b/runtime/src/psx_keybinds.c index 82b1a2dd5..1f2167e63 100644 --- a/runtime/src/psx_keybinds.c +++ b/runtime/src/psx_keybinds.c @@ -13,6 +13,7 @@ #include #include #include +#include /* Mouse-button pseudo-scancodes. Values sit above SDL's keyboard scancode * space (SDL_NUM_SCANCODES == 512), so they flow through the existing @@ -186,6 +187,17 @@ 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) { @@ -193,22 +205,20 @@ static void derive_ini_path(const char *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) { diff --git a/runtime/tests/test_keyboard_pad_chord.c b/runtime/tests/test_keyboard_pad_chord.c index cd5587830..1746f3f1c 100644 --- a/runtime/tests/test_keyboard_pad_chord.c +++ b/runtime/tests/test_keyboard_pad_chord.c @@ -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));