From 644757a65aaeef4c00a5774e2d0a1dbd7c8aa4cc Mon Sep 17 00:00:00 2001 From: "Andrei \"Akari\" Alexeyev" Date: Mon, 28 Aug 2017 14:51:05 +0300 Subject: [PATCH] gamepad settings improvements GUIDs are now used to identify devices. this allows reliable persistent configuration on multi-gamepad systems. changed the way the device listing in the options menu works. it's inactive when the gamepad system is disabled, and dynamically updated when it's enabled. as a result, the "bare" init state is now finally gone. this should work with minimal or no changes when the hotplugging events are properly handled. the "enable gamepad support" toggle is now effective immediately. the gamepad system is no longer restarted every time the user leaves the gamepad menu, unless they have changed the device. --- src/config.h | 2 +- src/gamepad.c | 157 ++++++++++++++++++++++++++++++++---------- src/gamepad.h | 8 ++- src/menu/options.c | 108 +++++++++++++++++++++++------ src/menu/options.h | 1 + src/menu/replayview.c | 14 +--- src/resource/font.c | 11 +++ src/resource/font.h | 1 + 8 files changed, 230 insertions(+), 72 deletions(-) diff --git a/src/config.h b/src/config.h index 190a4862..34b86c68 100644 --- a/src/config.h +++ b/src/config.h @@ -84,7 +84,7 @@ CONFIGDEF_FLOAT (BG_QUALITY, "bg_quality", 1.0) \ KEYDEFS \ CONFIGDEF_INT (GAMEPAD_ENABLED, "gamepad_enabled", 0) \ - CONFIGDEF_INT (GAMEPAD_DEVICE, "gamepad_device", 0) \ + CONFIGDEF_STRING (GAMEPAD_DEVICE, "gamepad_device", "default") \ CONFIGDEF_INT (GAMEPAD_AXIS_UD, "gamepad_axis_ud", 1) \ CONFIGDEF_INT (GAMEPAD_AXIS_LR, "gamepad_axis_lr", 0) \ CONFIGDEF_INT (GAMEPAD_AXIS_FREE, "gamepad_axis_free", 1) \ diff --git a/src/gamepad.c b/src/gamepad.c index dd8c5bbb..d0760d46 100644 --- a/src/gamepad.c +++ b/src/gamepad.c @@ -12,7 +12,8 @@ #include "global.h" static struct { - int initialized; + bool initialized; + int current_devnum; SDL_GameController *device; SDL_JoystickID instance; signed char *axis; @@ -42,14 +43,88 @@ static void gamepad_update_device_list(void) { } *idmap_ptr = i; - log_info("Gamepad device #%"PRIuMAX": %s", (uintmax_t)(idmap_ptr - gamepad.devices.id_map), SDL_GameControllerNameForIndex(i)); + int num = (int)(uintmax_t)(idmap_ptr - gamepad.devices.id_map); ++idmap_ptr; + gamepad.devices.count = (uintptr_t)(idmap_ptr - gamepad.devices.id_map); + + char guid[33] = {0}; + gamepad_deviceguid(num, guid, sizeof(guid)); + log_info("Gamepad device '%s' (#%i): %s", guid, num, SDL_GameControllerNameForIndex(i)); + } +} + +static int gamepad_find_device_by_guid(const char *guid_str, char *guid_out, size_t guid_out_sz, int *out_localdevnum) { + for(int i = 0; i < gamepad.devices.count; ++i) { + *guid_out = 0; + + SDL_JoystickGUID guid = SDL_JoystickGetDeviceGUID(gamepad.devices.id_map[i]); + SDL_JoystickGetGUIDString(guid, guid_out, guid_out_sz); + + if(!*guid_out) { + log_warn("Failed to read GUID of device %i: %s", gamepad.devices.id_map[i], SDL_GetError()); + continue; + } + + if(!strcasecmp(guid_str, guid_out) || !strcasecmp(guid_str, "default")) { + *out_localdevnum = i; + return gamepad.devices.id_map[i]; + } } - gamepad.devices.count = (uintptr_t)(idmap_ptr - gamepad.devices.id_map); + *out_localdevnum = -1; + return -1; +} + +static int gamepad_find_device(char *guid_out, size_t guid_out_sz, int *out_localdevnum) { + int dev; + const char *want_guid = config_get_str(CONFIG_GAMEPAD_DEVICE); + + dev = gamepad_find_device_by_guid(want_guid, guid_out, guid_out_sz, out_localdevnum); + + if(dev >= 0) { + return dev; + } + + if(strcasecmp(want_guid, "default")) { + log_warn("Requested device '%s' is not available, trying first usable", want_guid); + dev = gamepad_find_device_by_guid("default", guid_out, guid_out_sz, out_localdevnum); + + if(dev >= 0) { + return dev; + } + } + + *out_localdevnum = -1; + strcpy(guid_out, want_guid); + + return -1; +} + +static void gamepad_cfg_enabled_callback(ConfigIndex idx, ConfigValue v) { + config_set_int(idx, v.i); + + if(v.i) { + gamepad_init(); + } else { + gamepad_shutdown(); + } +} + +static void gamepad_setup_cfg_callbacks(void) { + static bool done = false; + + if(done) { + return; + } + + done = true; + + config_set_callback(CONFIG_GAMEPAD_ENABLED, gamepad_cfg_enabled_callback); } void gamepad_init(void) { + gamepad_setup_cfg_callbacks(); + if(!config_get_int(CONFIG_GAMEPAD_ENABLED) || gamepad.initialized) { return; } @@ -61,16 +136,19 @@ void gamepad_init(void) { gamepad_update_device_list(); - int dev = config_get_int(CONFIG_GAMEPAD_DEVICE); - if(dev < 0 || dev >= gamepad.devices.count) { - log_warn("Device #%i is not available", dev); + char guid[33]; + int dev = gamepad_find_device(guid, sizeof(guid), &gamepad.current_devnum); + + if(dev < 0) { + log_warn("Device '%s' is not available", guid); gamepad_shutdown(); return; } gamepad.device = SDL_GameControllerOpen(dev); + if(!gamepad.device) { - log_warn("Failed to open device %i: %s", dev, SDL_GetError()); + log_warn("Failed to open device '%s' (#%i): %s", guid, dev, SDL_GetError()); gamepad_shutdown(); return; } @@ -79,10 +157,11 @@ void gamepad_init(void) { gamepad.instance = SDL_JoystickInstanceID(joy); gamepad.axis = malloc(max(SDL_JoystickNumAxes(joy), 1)); - log_info("Using device #%i: %s", dev, gamepad_devicename(dev)); + log_info("Using device '%s' (#%i): %s", guid, dev, gamepad_devicename(dev)); SDL_GameControllerEventState(SDL_ENABLE); - gamepad.initialized = 1; + config_set_str(CONFIG_GAMEPAD_DEVICE, guid); + gamepad.initialized = true; } void gamepad_shutdown(void) { @@ -90,9 +169,7 @@ void gamepad_shutdown(void) { return; } - if(gamepad.initialized != 2) { - log_info("Disabled the gamepad subsystem"); - } + log_info("Disabled the gamepad subsystem"); if(gamepad.device) { SDL_GameControllerClose(gamepad.device); @@ -311,12 +388,42 @@ int gamepad_devicecount(void) { return gamepad.devices.count; } -const char* gamepad_devicename(int id) { - if(id < 0 || id >= gamepad.devices.count) { +const char* gamepad_devicename(int num) { + if(num < 0 || num >= gamepad.devices.count) { return NULL; } - return SDL_GameControllerNameForIndex(gamepad.devices.id_map[id]); + return SDL_GameControllerNameForIndex(gamepad.devices.id_map[num]); +} + +const void gamepad_deviceguid(int num, char *guid_str, size_t guid_str_sz) { + if(num < 0 || num >= gamepad.devices.count) { + return; + } + + SDL_JoystickGUID guid = SDL_JoystickGetDeviceGUID(gamepad.devices.id_map[num]); + SDL_JoystickGetGUIDString(guid, guid_str, guid_str_sz); +} + +int gamepad_numfromguid(const char *guid_str) { + for(int i = 0; i < gamepad.devices.count; ++i) { + char guid[33] = {0}; + gamepad_deviceguid(i, guid, sizeof(guid)); + + if(!strcasecmp(guid_str, guid)) { + return i; + } + } + + return -1; +} + +int gamepad_currentdevice(void) { + if(gamepad.initialized) { + return gamepad.current_devnum; + } + + return -1; } bool gamepad_buttonpressed(SDL_GameControllerButton btn) { @@ -339,26 +446,6 @@ bool gamepad_gamekeypressed(KeyIndex key) { return gamepad_buttonpressed(button); } -void gamepad_init_bare(void) { - if(gamepad.initialized) { - return; - } - - if(SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) { - log_warn("SDL_InitSubSystem() failed: %s", SDL_GetError()); - return; - } - - gamepad_update_device_list(); - gamepad.initialized = 2; -} - -void gamepad_shutdown_bare(void) { - if(gamepad.initialized == 2) { - gamepad_shutdown(); - } -} - const char* gamepad_button_name(SDL_GameControllerButton btn) { static const char *const map[] = { "A", diff --git a/src/gamepad.h b/src/gamepad.h index d78688b9..504e4678 100644 --- a/src/gamepad.h +++ b/src/gamepad.h @@ -17,11 +17,15 @@ void gamepad_init(void); void gamepad_shutdown(void); void gamepad_restart(void); -int gamepad_devicecount(void); float gamepad_axis_sens(int); -const char* gamepad_devicename(int); void gamepad_event(SDL_Event*, EventHandler, EventFlags, void*); +int gamepad_devicecount(void); +const char* gamepad_devicename(int); +const void gamepad_deviceguid(int num, char *guid_str, size_t guid_str_sz); +int gamepad_numfromguid(const char *guid_str); +int gamepad_currentdevice(void); + bool gamepad_buttonpressed(int btn); bool gamepad_gamekeypressed(KeyIndex key); diff --git a/src/menu/options.c b/src/menu/options.c index fe104ec4..4523d458 100644 --- a/src/menu/options.c +++ b/src/menu/options.c @@ -85,6 +85,40 @@ OptionBinding* bind_gpaxisbinding(int cfgentry) { return bind; } +static int bind_gpdev_get(OptionBinding *b) { + return gamepad_numfromguid(config_get_str(b->configentry)); +} + +static int bind_gpdev_set(OptionBinding *b, int v) { + char guid[33] = {0}; + gamepad_deviceguid(v, guid, sizeof(guid)); + + if(*guid) { + config_set_str(b->configentry, guid); + b->selected = v; + } + + return b->selected; +} + +// BT_GamepadDevice: dynamic device list +OptionBinding* bind_gpdevice(int cfgentry) { + OptionBinding *bind = bind_new(); + + bind->configentry = cfgentry; + bind->type = BT_GamepadDevice; + + bind->getter = bind_gpdev_get; + bind->setter = bind_gpdev_set; + + bind->valrange_min = 0; + bind->valrange_max = 0; // updated later + + bind->selected = gamepad_numfromguid(config_get_str(bind->configentry)); + + return bind; +} + // BT_StrValue: with a half-assed "textbox" OptionBinding* bind_stroption(ConfigIndex cfgentry) { OptionBinding *bind = bind_new(); @@ -180,13 +214,14 @@ int bind_getvalue(OptionBinding *b) { // Selects the next to current value of a BT_IntValue type binding int bind_setnext(OptionBinding *b) { - int s = b->selected +1; + int s = b->selected + 1; if(b->valrange_max) { if(s > b->valrange_max) s = b->valrange_min; - } else if(s >= b->valcount) + } else if(s >= b->valcount) { s = 0; + } return bind_setvalue(b, s); } @@ -198,8 +233,9 @@ int bind_setprev(OptionBinding *b) { if(b->valrange_max) { if(s < b->valrange_min) s = b->valrange_max; - } else if(s < 0) + } else if(s < 0) { s = b->valcount - 1; + } return bind_setvalue(b, s); } @@ -381,10 +417,14 @@ void bind_setvaluerange_fancy(OptionBinding *b, int ma) { } } -bool gamepad_sens_depencence(void) { +static bool gamepad_sens_depencence(void) { return config_get_int(CONFIG_GAMEPAD_AXIS_FREE); } +static bool gamepad_enabled_depencence(void) { + return config_get_int(CONFIG_GAMEPAD_ENABLED); +} + void options_sub_gamepad_controls(MenuData *parent, void *arg) { MenuData menu, *m; m = &menu; @@ -432,7 +472,6 @@ void options_sub_gamepad_controls(MenuData *parent, void *arg) { menu_loop(m); parent->frames = 0; - gamepad_restart(); } void options_sub_gamepad(MenuData *parent, void *arg) { @@ -447,26 +486,12 @@ void options_sub_gamepad(MenuData *parent, void *arg) { ); bind_onoff(b); add_menu_entry(m, "Device", do_nothing, - b = bind_option(CONFIG_GAMEPAD_DEVICE, bind_common_intget, bind_common_intset) - ); b->displaysingle = true; + b = bind_gpdevice(CONFIG_GAMEPAD_DEVICE) + ); bind_setdependence(b, gamepad_enabled_depencence); add_menu_separator(m); add_menu_entry(m, "Customize controls…", options_sub_gamepad_controls, NULL); - gamepad_init_bare(); - int cnt = gamepad_devicecount(); - int i; for(i = 0; i < cnt; ++i) { - char buf[50]; - snprintf(buf, sizeof(buf), "#%i: %s", i+1, gamepad_devicename(i)); - bind_addvalue(b, buf); - } - - if(!i) { - bind_addvalue(b, "No devices available"); - b->selected = 0; - } - gamepad_shutdown_bare(); - add_menu_separator(m); add_menu_entry(m, "X axis", do_nothing, @@ -499,9 +524,16 @@ void options_sub_gamepad(MenuData *parent, void *arg) { add_menu_separator(m); add_menu_entry(m, "Back", menu_commonaction_close, NULL); + char *gpdev = strdup(config_get_str(CONFIG_GAMEPAD_DEVICE)); + menu_loop(m); parent->frames = 0; - gamepad_restart(); + + if(config_get_int(CONFIG_GAMEPAD_ENABLED) && strcasecmp(config_get_str(CONFIG_GAMEPAD_DEVICE), gpdev)) { + gamepad_restart(); + } + + free(gpdev); } void options_sub_controls(MenuData *parent, void *arg) { @@ -722,6 +754,36 @@ void draw_options_menu(MenuData *menu) { break; } + case BT_GamepadDevice: { + if(bind_isactive(bind)) { + // XXX: I'm not exactly a huge fan of fixing up state in drawing code, but it seems the way to go for now... + bind->valrange_max = gamepad_devicecount() - 1; + + if(bind->selected < 0 || bind->selected > bind->valrange_max) { + bind->selected = gamepad_currentdevice(); + + if(bind->selected < 0) { + bind->selected = 0; + } + } + + char *txt; + char buf[64]; + + if(bind->valrange_max > 0) { + snprintf(buf, sizeof(buf), "#%i: %s", bind->selected + 1, gamepad_devicename(bind->selected)); + shorten_text_up_to_width(buf, (SCREEN_W - 220) / 2, _fonts.standard); + txt = buf; + } else { + txt = "No devices available"; + } + + draw_text(AL_Right, origin, 20*i, txt, _fonts.standard); + } + + break; + } + case BT_GamepadKeyBinding: case BT_GamepadAxisBinding: { bool is_axis = (bind->type == BT_GamepadAxisBinding); @@ -946,6 +1008,7 @@ static void options_input_event(EventType type, int state, void *arg) { play_ui_sound("generic_shot"); if(bind) { switch(bind->type) { + case BT_GamepadDevice: case BT_IntValue: case BT_Resolution: bind_setprev(bind); @@ -965,6 +1028,7 @@ static void options_input_event(EventType type, int state, void *arg) { play_ui_sound("generic_shot"); if(bind) { switch(bind->type) { + case BT_GamepadDevice: case BT_IntValue: case BT_Resolution: bind_setnext(bind); diff --git a/src/menu/options.h b/src/menu/options.h index cdaa5f4c..88a1abca 100644 --- a/src/menu/options.h +++ b/src/menu/options.h @@ -30,6 +30,7 @@ typedef enum BindingType { BT_Scale, BT_GamepadKeyBinding, BT_GamepadAxisBinding, + BT_GamepadDevice, } BindingType; typedef struct OptionBinding { diff --git a/src/menu/replayview.c b/src/menu/replayview.c index c79f4ae0..82df54e2 100644 --- a/src/menu/replayview.c +++ b/src/menu/replayview.c @@ -82,17 +82,6 @@ static void replayview_freearg(void *a) { free(ctx); } -static void shorten(char *s, float width) { - while(stringwidth(s, _fonts.standard) > width) { - s[strlen(s) - 1] = 0; - - int l = strlen(s); - for(int i = 0; i < 3; ++i) { - s[l - i - 1] = '.'; - } - } -} - static void replayview_draw_stagemenu(MenuData *m) { // this context is shared with the parent menu ReplayviewContext *ctx = m->context; @@ -196,7 +185,8 @@ static void replayview_drawitem(void *n, int item, int cnt) { break; } - shorten(tmp, csize); + shorten_text_up_to_width(tmp, csize, _fonts.standard); + switch(a) { case AL_Center: o += csize * 0.5 - stringwidth(tmp, _fonts.standard) * 0.5; break; case AL_Right: o += csize - stringwidth(tmp, _fonts.standard); break; diff --git a/src/resource/font.c b/src/resource/font.c index 459ef96d..265548fb 100644 --- a/src/resource/font.c +++ b/src/resource/font.c @@ -224,3 +224,14 @@ int charwidth(char c, TTF_Font *font) { s[1] = 0; return stringwidth(s, font); } + +void shorten_text_up_to_width(char *s, float width, TTF_Font *font) { + while(stringwidth(s, font) > width) { + s[strlen(s) - 1] = 0; + + int l = strlen(s); + for(int i = 0; i < 3; ++i) { + s[l - i - 1] = '.'; + } + } +} diff --git a/src/resource/font.h b/src/resource/font.h index 5ebb2f2e..8ba05a33 100644 --- a/src/resource/font.h +++ b/src/resource/font.h @@ -45,6 +45,7 @@ void draw_text_prerendered(Alignment align, float x, float y, SDL_Surface *surf) int stringwidth(char *s, TTF_Font *font); int stringheight(char *s, TTF_Font *font); int charwidth(char c, TTF_Font *font); +void shorten_text_up_to_width(char *s, float width, TTF_Font *font); void init_fonts(void); void uninit_fonts(void);