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);